From 42bc0bed279ceafa40f72714ae18724abfff85c0 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 17 Apr 2026 11:34:08 +0200 Subject: [PATCH] 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) --- .../webapp/src/containers/Items/ItemForm.tsx | 32 +-- ...rySection.tsx => ItemFormBasicSection.tsx} | 141 +++++----- .../src/containers/Items/ItemFormBody.tsx | 246 ------------------ .../src/containers/Items/ItemFormContent.tsx | 44 ++++ .../src/containers/Items/ItemFormFields.tsx | 30 +++ .../Items/ItemFormFloatingActions.tsx | 130 +++++---- .../src/containers/Items/ItemFormFormik.tsx | 32 ++- .../Items/ItemFormInventorySection.tsx | 62 ++--- .../Items/ItemFormPurchasingSection.tsx | 142 ++++++++++ .../containers/Items/ItemFormSectionTitle.tsx | 14 + .../Items/ItemFormSellingSection.tsx | 135 ++++++++++ .../webapp/src/style/pages/Items/Form.scss | 89 +------ 12 files changed, 564 insertions(+), 533 deletions(-) rename packages/webapp/src/containers/Items/{ItemFormPrimarySection.tsx => ItemFormBasicSection.tsx} (51%) delete mode 100644 packages/webapp/src/containers/Items/ItemFormBody.tsx create mode 100644 packages/webapp/src/containers/Items/ItemFormContent.tsx create mode 100644 packages/webapp/src/containers/Items/ItemFormFields.tsx create mode 100644 packages/webapp/src/containers/Items/ItemFormPurchasingSection.tsx create mode 100644 packages/webapp/src/containers/Items/ItemFormSectionTitle.tsx create mode 100644 packages/webapp/src/containers/Items/ItemFormSellingSection.tsx diff --git a/packages/webapp/src/containers/Items/ItemForm.tsx b/packages/webapp/src/containers/Items/ItemForm.tsx index 271673a27..58b5cf0ff 100644 --- a/packages/webapp/src/containers/Items/ItemForm.tsx +++ b/packages/webapp/src/containers/Items/ItemForm.tsx @@ -1,14 +1,14 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import ItemFormFormik from './ItemFormFormik'; import { useDashboardPageTitle } from '@/hooks/state'; import { useItemFormContext, ItemFormProvider } from './ItemFormProvider'; -import { DashboardInsider, DashboardCard } from '@/components'; +import { DashboardInsider } from '@/components'; +import { Box } from '@/components'; /** * Item form dashboard title. @@ -39,9 +39,9 @@ function ItemFormPageLoading({ children }) { const { isFormLoading } = useItemFormContext(); return ( - + {children} - + ); } @@ -59,36 +59,18 @@ export default function ItemForm({ itemId }) { history.push('/items'); } }; - // Handle cancel button click. - const handleFormCancel = () => { - history.goBack(); - }; return ( - - + - + ); } - -const DashboardItemFormPageInsider = styled(DashboardInsider)` - padding-bottom: 64px; -`; - -const ItemFormPageFormik = styled(ItemFormFormik)` - .page-form { - &__floating-actions { - margin-left: -40px; - margin-right: -40px; - } - } -`; diff --git a/packages/webapp/src/containers/Items/ItemFormPrimarySection.tsx b/packages/webapp/src/containers/Items/ItemFormBasicSection.tsx similarity index 51% rename from packages/webapp/src/containers/Items/ItemFormPrimarySection.tsx rename to packages/webapp/src/containers/Items/ItemFormBasicSection.tsx index 58e241a87..cbfdd7f5c 100644 --- a/packages/webapp/src/containers/Items/ItemFormPrimarySection.tsx +++ b/packages/webapp/src/containers/Items/ItemFormBasicSection.tsx @@ -3,41 +3,31 @@ import React, { useEffect, useRef } from 'react'; import { FormGroup, RadioGroup, - Classes, Radio, Position, - MenuItem, + Checkbox, } from '@blueprintjs/core'; import { ErrorMessage, FastField } from 'formik'; -import { CLASSES } from '@/constants/classes'; import { Hint, - Col, - Row, FieldRequiredHint, FormattedMessage as T, FormattedHTMLMessage, FFormGroup, FSelect, FInputGroup, + Box, } from '@/components'; -import classNames from 'classnames'; import { useItemFormContext } from './ItemFormProvider'; import { handleStringChange, inputIntent } from '@/utils'; -// import { categoriesFieldShouldUpdate } from './utils'; +import { ItemFormSectionTitle } from './ItemFormSectionTitle'; -/** - * Item form primary section. - */ -export default function ItemFormPrimarySection() { - // Item form context. +export function ItemFormBasicSection() { const { isNewMode, item, itemsCategories } = useItemFormContext(); - const nameFieldRef = useRef(null); useEffect(() => { - // Auto focus item name field once component mount. if (nameFieldRef.current) { nameFieldRef.current.focus(); } @@ -45,17 +35,19 @@ export default function ItemFormPrimarySection() { const itemTypeHintContent = ( <> -
+
-
+
); return ( -
+ + Basic details + {/*----------- Item type ----------*/} {({ form, field: { value }, meta: { touched, error } }) => ( @@ -91,61 +83,66 @@ export default function ItemFormPrimarySection() { )} - - - {/*----------- Item name ----------*/} - } - labelInfo={} - inline={true} - fastField - > - (nameFieldRef.current = ref)} - fastField + {/*----------- Item name ----------*/} + } + labelInfo={} + inline={true} + fill + fastField + > + (nameFieldRef.current = ref)} + fastField + fill + /> + + + {/*----------- SKU ----------*/} + } + inline={true} + fill + fastField + > + + + + {/*----------- Item category ----------*/} + } + inline={true} + fill + > + } + popoverProps={{ minimal: true, captureDismiss: true }} + fill + /> + + + {/*----------- Active ----------*/} + + {({ field }) => ( + + } + name={'active'} + {...field} /> - - - {/*----------- SKU ----------*/} - } - inline={true} - fastField - > - - - - {/*----------- Item category ----------*/} - } - inline={true} - > - } - popoverProps={{ minimal: true, captureDismiss: true }} - /> - - - - - {/* */} - - -
+ + )} + + ); } diff --git a/packages/webapp/src/containers/Items/ItemFormBody.tsx b/packages/webapp/src/containers/Items/ItemFormBody.tsx deleted file mode 100644 index 87ab384e2..000000000 --- a/packages/webapp/src/containers/Items/ItemFormBody.tsx +++ /dev/null @@ -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 ( -
- - - {/*------------- Purchasable checbox ------------- */} - - {({ form, field }) => ( - - - - - } - name={'sellable'} - {...field} - /> - - )} - - - {/*------------- Selling price ------------- */} - } - inline - fastField - > - - - - - - - {/*------------- Selling account ------------- */} - } - name={'sell_account_id'} - labelInfo={ - } /> - } - inline={true} - items={accounts} - sellable={values.sellable} - shouldUpdate={sellAccountFieldShouldUpdate} - fastField={true} - > - } - disabled={!values.sellable} - filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]} - fill={true} - allowCreate={true} - fastField={true} - /> - - - {/*------------- Sell Tax Rate ------------- */} - - - - - } - inline={true} - sellable={values.sellable} - shouldUpdate={sellDescriptionFieldShouldUpdate} - fastField - > - - - - - - {/*------------- Sellable checkbox ------------- */} - - {({ field }) => ( - - - - - } - {...field} - /> - - )} - - - {/*------------- Cost price ------------- */} - } - inline - fastField - > - - - - - - - - {/*------------- Cost account ------------- */} - } - labelInfo={ - } /> - } - inline={true} - fastField={true} - > - } - filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]} - popoverFill={true} - allowCreate={true} - fastField={true} - disabled={!values.purchasable} - purchasable={values.purchasable} - shouldUpdate={costAccountFieldShouldUpdate} - /> - - - {/*------------- Purchase Tax Rate ------------- */} - - - - - } - className={'form-group--purchase-description'} - helperText={} - inline={true} - purchasable={values.purchasable} - shouldUpdate={purchaseDescFieldShouldUpdate} - > - - - - -
- ); -} - -export default compose(withCurrentOrganization())(ItemFormBody); diff --git a/packages/webapp/src/containers/Items/ItemFormContent.tsx b/packages/webapp/src/containers/Items/ItemFormContent.tsx new file mode 100644 index 000000000..77c548d9b --- /dev/null +++ b/packages/webapp/src/containers/Items/ItemFormContent.tsx @@ -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 ( + + + + + + + + + + + + + + ) +} diff --git a/packages/webapp/src/containers/Items/ItemFormFields.tsx b/packages/webapp/src/containers/Items/ItemFormFields.tsx new file mode 100644 index 000000000..6c1161aa4 --- /dev/null +++ b/packages/webapp/src/containers/Items/ItemFormFields.tsx @@ -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 ( + + + + + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Items/ItemFormFloatingActions.tsx b/packages/webapp/src/containers/Items/ItemFormFloatingActions.tsx index d7656b155..5c3c99784 100644 --- a/packages/webapp/src/containers/Items/ItemFormFloatingActions.tsx +++ b/packages/webapp/src/containers/Items/ItemFormFloatingActions.tsx @@ -1,89 +1,83 @@ // @ts-nocheck -import React from 'react'; +import { + Intent, + Button, + ButtonGroup, + Popover, + PopoverInteractionKind, + Position, + Menu, + MenuItem, +} from '@blueprintjs/core'; import styled from 'styled-components'; -import classNames from 'classnames'; -import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core'; -import { FastField, useFormikContext } from 'formik'; -import { CLASSES } from '@/constants/classes'; +import { useFormikContext } from 'formik'; + +import { Group, Icon, FormattedMessage as T } from '@/components'; 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 }) { - // Item form context. - const { setSubmitPayload, isNewMode } = useItemFormContext(); - +export function ItemFormFloatingActions() { // Formik context. const { isSubmitting, submitForm } = useFormikContext(); - // Handle cancel button click. - const handleCancelBtnClick = (event) => { - saveInvoke(onCancel, event); - }; + // Item form context. + const { isNewMode, setSubmitPayload } = useItemFormContext(); - // Handle submit button click. - const handleSubmitBtnClick = (event) => { + // Handle the submit button. + const handleSubmitBtnClick = () => { setSubmitPayload({ redirect: true }); }; - // Handle submit & new button click. - const handleSubmitAndNewBtnClick = (event) => { - setSubmitPayload({ redirect: false }); + // Handle the submit & new button click. + const handleSubmitAndNewClick = () => { submitForm(); + setSubmitPayload({ redirect: false }); }; return ( - - - {isNewMode ? : } - - - - - - - {/*----------- Active ----------*/} - - {({ field }) => ( - - } - name={'active'} - {...field} - /> - - )} - - + + + {/* ----------- Save and New ----------- */} +