diff --git a/src/common/drawers.js b/src/common/drawers.js
index 449f51485..3a1b21032 100644
--- a/src/common/drawers.js
+++ b/src/common/drawers.js
@@ -10,4 +10,8 @@ export const DRAWERS = {
BILL_DRAWER: 'bill-drawer',
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer',
CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-drawer',
+
+ QUICK_WRITE_VENDOR: 'quick-write-vendor',
+ QUICK_CREATE_CUSTOMER: 'quick-create-customer',
+ QUICK_CREATE_ITEM: 'quick-create-item',
};
diff --git a/src/components/AccountsSelectList.js b/src/components/AccountsSelectList.js
index 75a7d3e09..9ed337284 100644
--- a/src/components/AccountsSelectList.js
+++ b/src/components/AccountsSelectList.js
@@ -1,13 +1,55 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
-import { MenuItemNestedText, FormattedMessage as T } from 'components';
+import * as R from 'ramda';
import classNames from 'classnames';
+
+import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes';
-export default function AccountsSelectList({
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+// Create new account renderer.
+const createNewItemRenderer = (query, active, handleClick) => {
+ return (
+
+ );
+};
+
+// Create new item from the given query string.
+const createNewItemFromQuery = (name) => {
+ return {
+ name,
+ };
+};
+
+// Filters accounts items.
+const filterAccountsPredicater = (query, account, _index, exactMatch) => {
+ const normalizedTitle = account.name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
+ }
+};
+
+/**
+ * Accounts select list.
+ */
+function AccountsSelectList({
+ // #withDialogActions
+ openDialog,
+
+ // #ownProps
accounts,
initialAccountId,
selectedAccountId,
@@ -21,6 +63,8 @@ export default function AccountsSelectList({
filterByNormal,
filterByRootTypes,
+ allowCreate,
+
buttonProps = {},
}) {
const flattenAccounts = useMemo(
@@ -51,6 +95,7 @@ export default function AccountsSelectList({
[initialAccountId, filteredAccounts],
);
+ // Select account item.
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
@@ -76,31 +121,25 @@ export default function AccountsSelectList({
);
}, []);
- const onAccountSelect = useCallback(
+ // Handle the account item select.
+ const handleAccountSelect = useCallback(
(account) => {
- setSelectedAccount({ ...account });
- onAccountSelected && onAccountSelected(account);
- },
- [setSelectedAccount, onAccountSelected],
- );
-
- // Filters accounts items.
- const filterAccountsPredicater = useCallback(
- (query, account, _index, exactMatch) => {
- const normalizedTitle = account.name.toLowerCase();
- const normalizedQuery = query.toLowerCase();
-
- if (exactMatch) {
- return normalizedTitle === normalizedQuery;
+ if (account.id) {
+ setSelectedAccount({ ...account });
+ onAccountSelected && onAccountSelected(account);
} else {
- return (
- `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
- );
+ openDialog('account-form');
}
},
- [],
+ [setSelectedAccount, onAccountSelected, openDialog],
);
+ // Maybe inject new item props to select component.
+ const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
+ const maybeCreateNewItemFromQuery = allowCreate
+ ? createNewItemFromQuery
+ : null;
+
return (
+ );
+}
diff --git a/src/components/Contacts/CustomerSelectField.js b/src/components/Contacts/CustomerSelectField.js
new file mode 100644
index 000000000..322e7b85b
--- /dev/null
+++ b/src/components/Contacts/CustomerSelectField.js
@@ -0,0 +1,116 @@
+import React, { useCallback, useState, useEffect, useMemo } from 'react';
+import { FormattedMessage as T } from 'components';
+import intl from 'react-intl-universal';
+import * as R from 'ramda';
+
+import { MenuItem, Button } from '@blueprintjs/core';
+import { Select } from '@blueprintjs/select';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+
+import {
+ itemPredicate,
+ handleContactRenderer,
+ createNewItemRenderer,
+ createNewItemFromQuery,
+} from './utils';
+
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+
+import { DRAWERS } from 'common/drawers';
+
+function CustomerSelectField({
+ // #withDrawerActions
+ openDrawer,
+
+ // #ownProps
+ contacts,
+ initialContactId,
+ selectedContactId,
+ defaultSelectText = ,
+ onContactSelected,
+ popoverFill = false,
+ disabled = false,
+ allowCreate,
+ buttonProps,
+
+ ...restProps
+}) {
+ const localContacts = useMemo(
+ () =>
+ contacts.map((contact) => ({
+ ...contact,
+ _id: `${contact.id}_${contact.contact_type}`,
+ })),
+ [contacts],
+ );
+
+ const initialContact = useMemo(
+ () => contacts.find((a) => a.id === initialContactId),
+ [initialContactId, contacts],
+ );
+
+ const [selecetedContact, setSelectedContact] = useState(
+ initialContact || null,
+ );
+
+ useEffect(() => {
+ if (typeof selectedContactId !== 'undefined') {
+ const account = selectedContactId
+ ? contacts.find((a) => a.id === selectedContactId)
+ : null;
+ setSelectedContact(account);
+ }
+ }, [selectedContactId, contacts, setSelectedContact]);
+
+ const handleContactSelect = useCallback(
+ (contact) => {
+ if (contact.id) {
+ setSelectedContact({ ...contact });
+ onContactSelected && onContactSelected(contact);
+ } else {
+ openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
+ }
+ },
+ [setSelectedContact, onContactSelected, openDrawer],
+ );
+
+ // Maybe inject create new item props to suggest component.
+ const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
+ const maybeCreateNewItemFromQuery = allowCreate
+ ? createNewItemFromQuery
+ : null;
+
+ return (
+ } />}
+ itemRenderer={handleContactRenderer}
+ itemPredicate={itemPredicate}
+ filterable={true}
+ disabled={disabled}
+ onItemSelect={handleContactSelect}
+ popoverProps={{ minimal: true, usePortal: !popoverFill }}
+ className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
+ [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
+ })}
+ inputProps={{
+ placeholder: intl.get('filter_'),
+ }}
+ createNewItemRenderer={maybeCreateNewItemRenderer}
+ createNewItemFromQuery={maybeCreateNewItemFromQuery}
+ createNewItemPosition={'top'}
+ {...restProps}
+ >
+
+
+ );
+}
+
+export default R.compose(withDrawerActions)(CustomerSelectField);
diff --git a/src/components/Contacts/VendorSelectField.js b/src/components/Contacts/VendorSelectField.js
new file mode 100644
index 000000000..1f0560f74
--- /dev/null
+++ b/src/components/Contacts/VendorSelectField.js
@@ -0,0 +1,115 @@
+import React, { useCallback, useState, useEffect, useMemo } from 'react';
+import { FormattedMessage as T } from 'components';
+import intl from 'react-intl-universal';
+import * as R from 'ramda';
+
+import { MenuItem, Button } from '@blueprintjs/core';
+import { Select } from '@blueprintjs/select';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+
+import {
+ itemPredicate,
+ handleContactRenderer,
+ createNewItemFromQuery,
+ createNewItemRenderer,
+} from './utils';
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+
+import { DRAWERS } from 'common/drawers';
+
+function VendorSelectField({
+ // #withDrawerActions
+ openDrawer,
+
+ // #ownProps
+ contacts,
+ initialContactId,
+ selectedContactId,
+ defaultSelectText = ,
+ onContactSelected,
+ popoverFill = false,
+ disabled = false,
+ allowCreate,
+ buttonProps,
+
+ ...restProps
+}) {
+ const localContacts = useMemo(
+ () =>
+ contacts.map((contact) => ({
+ ...contact,
+ _id: `${contact.id}_${contact.contact_type}`,
+ })),
+ [contacts],
+ );
+
+ const initialContact = useMemo(
+ () => contacts.find((a) => a.id === initialContactId),
+ [initialContactId, contacts],
+ );
+
+ const [selecetedContact, setSelectedContact] = useState(
+ initialContact || null,
+ );
+
+ useEffect(() => {
+ if (typeof selectedContactId !== 'undefined') {
+ const account = selectedContactId
+ ? contacts.find((a) => a.id === selectedContactId)
+ : null;
+ setSelectedContact(account);
+ }
+ }, [selectedContactId, contacts, setSelectedContact]);
+
+ const handleContactSelect = useCallback(
+ (contact) => {
+ if (contact.id) {
+ setSelectedContact({ ...contact });
+ onContactSelected && onContactSelected(contact);
+ } else {
+ openDrawer(DRAWERS.QUICK_WRITE_VENDOR);
+ }
+ },
+ [setSelectedContact, onContactSelected, openDrawer],
+ );
+
+ // Maybe inject create new item props to suggest component.
+ const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
+ const maybeCreateNewItemFromQuery = allowCreate
+ ? createNewItemFromQuery
+ : null;
+
+ return (
+ } />}
+ itemRenderer={handleContactRenderer}
+ itemPredicate={itemPredicate}
+ filterable={true}
+ disabled={disabled}
+ onItemSelect={handleContactSelect}
+ popoverProps={{ minimal: true, usePortal: !popoverFill }}
+ className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
+ [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
+ })}
+ inputProps={{
+ placeholder: intl.get('filter_'),
+ }}
+ createNewItemRenderer={maybeCreateNewItemRenderer}
+ createNewItemFromQuery={maybeCreateNewItemFromQuery}
+ createNewItemPosition={'top'}
+ {...restProps}
+ >
+
+
+ );
+}
+
+export default R.compose(withDrawerActions)(VendorSelectField);
diff --git a/src/components/Contacts/index.js b/src/components/Contacts/index.js
new file mode 100644
index 000000000..d6c1821c2
--- /dev/null
+++ b/src/components/Contacts/index.js
@@ -0,0 +1,5 @@
+import ContactSelectField from './ContactSelectField';
+import CustomerSelectField from './CustomerSelectField';
+import VendorSelectField from './VendorSelectField';
+
+export { ContactSelectField, CustomerSelectField, VendorSelectField };
diff --git a/src/components/Contacts/utils.js b/src/components/Contacts/utils.js
new file mode 100644
index 000000000..59d0a4a8c
--- /dev/null
+++ b/src/components/Contacts/utils.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { MenuItem } from '@blueprintjs/core';
+
+// Filter Contact List
+export const itemPredicate = (query, contact, index, exactMatch) => {
+ const normalizedTitle = contact.display_name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return (
+ `${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
+ );
+ }
+};
+
+export const handleContactRenderer = (contact, { handleClick }) => (
+
+);
+
+// Creates a new item from query.
+export const createNewItemFromQuery = (name) => {
+ return {
+ name,
+ };
+};
+
+// Handle quick create new customer.
+export const createNewItemRenderer = (query, active, handleClick) => {
+ return (
+
+ );
+};
diff --git a/src/components/Dashboard/DashboardContent.js b/src/components/Dashboard/DashboardContent.js
index 3c75d2288..739b405e7 100644
--- a/src/components/Dashboard/DashboardContent.js
+++ b/src/components/Dashboard/DashboardContent.js
@@ -2,7 +2,6 @@ import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
-import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => {
@@ -11,7 +10,6 @@ export default React.forwardRef(({}, ref) => {
-
);
diff --git a/src/components/DataTableCells/AccountsListFieldCell.js b/src/components/DataTableCells/AccountsListFieldCell.js
index 1f27453a1..690e64b24 100644
--- a/src/components/DataTableCells/AccountsListFieldCell.js
+++ b/src/components/DataTableCells/AccountsListFieldCell.js
@@ -17,6 +17,8 @@ export default function AccountCellRenderer({
accountsDataProp,
filterAccountsByRootTypes,
filterAccountsByTypes,
+ fieldProps,
+ formGroupProps,
},
row: { index, original },
cell: { value: initialValue },
@@ -53,6 +55,7 @@ export default function AccountCellRenderer({
'form-group--account',
Classes.FILL,
)}
+ {...formGroupProps}
>
);
diff --git a/src/components/DataTableCells/ItemsListCell.js b/src/components/DataTableCells/ItemsListCell.js
index 9ff7ddae1..ed6c024c6 100644
--- a/src/components/DataTableCells/ItemsListCell.js
+++ b/src/components/DataTableCells/ItemsListCell.js
@@ -1,15 +1,17 @@
import React, { useCallback, useRef } from 'react';
-// import ItemsListField from 'components/ItemsListField';
-import ItemsSuggestField from 'components/ItemsSuggestField';
import classNames from 'classnames';
-
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
+import ItemsSuggestField from 'components/ItemsSuggestField';
+
import { useCellAutoFocus } from 'hooks';
+/**
+ * Items list cell.
+ */
export default function ItemsListCell({
- column: { id, filterSellable, filterPurchasable },
+ column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
row: { index },
cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus },
@@ -19,6 +21,7 @@ export default function ItemsListCell({
// Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index);
+ // Handle the item selected.
const handleItemSelected = useCallback(
(item) => {
updateData(index, id, item.id);
@@ -32,6 +35,7 @@ export default function ItemsListCell({
);
diff --git a/src/components/Drawer/Drawer.js b/src/components/Drawer/Drawer.js
index 96f7e0b5a..fe660deb2 100644
--- a/src/components/Drawer/Drawer.js
+++ b/src/components/Drawer/Drawer.js
@@ -3,6 +3,7 @@ import { Position, Drawer } from '@blueprintjs/core';
import 'style/components/Drawer.scss';
+import { DrawerProvider } from './DrawerProvider';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { compose } from 'utils';
@@ -27,7 +28,7 @@ function DrawerComponent(props) {
portalClassName={'drawer-portal'}
{...props}
>
- {children}
+ {children}
);
}
diff --git a/src/components/Drawer/DrawerProvider.js b/src/components/Drawer/DrawerProvider.js
new file mode 100644
index 000000000..7b086cd32
--- /dev/null
+++ b/src/components/Drawer/DrawerProvider.js
@@ -0,0 +1,16 @@
+import React, { createContext, useContext } from 'react';
+
+const DrawerContext = createContext();
+
+/**
+ * Account form provider.
+ */
+function DrawerProvider({ ...props }) {
+ const provider = { ...props };
+
+ return ;
+}
+
+const useDrawerContext = () => useContext(DrawerContext);
+
+export { DrawerProvider, useDrawerContext };
diff --git a/src/components/DrawersContainer.js b/src/components/DrawersContainer.js
index f240fd1bf..135dcb8a3 100644
--- a/src/components/DrawersContainer.js
+++ b/src/components/DrawersContainer.js
@@ -14,6 +14,9 @@ import CustomerDetailsDrawer from '../containers/Drawers/CustomerDetailsDrawer';
import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer';
import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer';
import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTransactionDetailDrawer';
+import QuickCreateCustomerDrawer from '../containers/Drawers/QuickCreateCustomerDrawer';
+import QuickCreateItemDrawer from '../containers/Drawers/QuickCreateItemDrawer';
+import QuickWriteVendorDrawer from '../containers/Drawers/QuickWriteVendorDrawer';
import { DRAWERS } from 'common/drawers';
@@ -38,7 +41,12 @@ export default function DrawersContainer() {
-
+
+
+
+
);
}
diff --git a/src/components/ItemsSuggestField.js b/src/components/ItemsSuggestField.js
index a7223bfe9..7473a6bb5 100644
--- a/src/components/ItemsSuggestField.js
+++ b/src/components/ItemsSuggestField.js
@@ -1,13 +1,68 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
-import classNames from 'classnames';
-import { CLASSES } from 'common/classes';
-
import { Suggest } from '@blueprintjs/select';
+import classNames from 'classnames';
+import * as R from 'ramda';
+
+import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'components';
-export default function ItemsSuggestField({
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+
+import { DRAWERS } from 'common/drawers';
+
+// Creates a new item from query.
+const createNewItemFromQuery = (name) => {
+ return {
+ name,
+ };
+};
+
+// Handle quick create new customer.
+const createNewItemRenderer = (query, active, handleClick) => {
+ return (
+
+ );
+};
+
+// Item renderer.
+const itemRenderer = (item, { modifiers, handleClick }) => (
+
+);
+
+// Filters items.
+const filterItemsPredicater = (query, item, _index, exactMatch) => {
+ const normalizedTitle = item.name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
+ }
+};
+
+// Handle input value renderer.
+const handleInputValueRenderer = (inputValue) => {
+ if (inputValue) {
+ return inputValue.name.toString();
+ }
+ return '';
+};
+
+function ItemsSuggestField({
items,
initialItemId,
selectedItemId,
@@ -18,6 +73,10 @@ export default function ItemsSuggestField({
sellable = false,
purchasable = false,
popoverFill = false,
+
+ allowCreate = true,
+
+ openDrawer,
...suggestProps
}) {
// Filters items based on filter props.
@@ -36,28 +95,23 @@ export default function ItemsSuggestField({
// Find initial item object.
const initialItem = useMemo(
() => filteredItems.some((a) => a.id === initialItemId),
- [initialItemId],
+ [initialItemId, filteredItems],
);
const [selectedItem, setSelectedItem] = useState(initialItem || null);
const onItemSelect = useCallback(
(item) => {
- setSelectedItem({ ...item });
- onItemSelected && onItemSelected(item);
+ if (item.id) {
+ setSelectedItem({ ...item });
+ onItemSelected && onItemSelected(item);
+ } else {
+ openDrawer(DRAWERS.QUICK_CREATE_ITEM);
+ }
},
- [setSelectedItem, onItemSelected],
+ [setSelectedItem, onItemSelected, openDrawer],
);
- const itemRenderer = useCallback((item, { modifiers, handleClick }) => (
-
- ));
-
useEffect(() => {
if (typeof selectedItemId !== 'undefined') {
const item = selectedItemId
@@ -67,27 +121,12 @@ export default function ItemsSuggestField({
}
}, [selectedItemId, filteredItems, setSelectedItem]);
- const handleInputValueRenderer = (inputValue) => {
- if (inputValue) {
- return inputValue.name.toString();
- }
- return '';
- };
+ // Maybe inject create new item props to suggest component.
+ const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
+ const maybeCreateNewItemFromQuery = allowCreate
+ ? createNewItemFromQuery
+ : null;
- // Filters items.
- const filterItemsPredicater = useCallback(
- (query, item, _index, exactMatch) => {
- const normalizedTitle = item.name.toLowerCase();
- const normalizedQuery = query.toLowerCase();
-
- if (exactMatch) {
- return normalizedTitle === normalizedQuery;
- } else {
- return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
- }
- },
- [],
- );
return (
);
}
+
+export default R.compose(withDrawerActions)(ItemsSuggestField);
diff --git a/src/components/index.js b/src/components/index.js
index b8ca111f4..f30398145 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -86,6 +86,7 @@ export * from './Datatable/CellForceWidth';
export * from './Button';
export * from './IntersectionObserver';
export * from './SMSPreview';
+export * from './Contacts';
const Hint = FieldHint;
diff --git a/src/containers/Accounting/MakeJournal/components.js b/src/containers/Accounting/MakeJournal/components.js
index e7a5dbf86..f79926091 100644
--- a/src/containers/Accounting/MakeJournal/components.js
+++ b/src/containers/Accounting/MakeJournal/components.js
@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
className: 'account',
disableSortBy: true,
width: 160,
+ fieldProps: { allowCreate: true }
},
{
Header: CreditHeaderCell,
diff --git a/src/containers/Customers/CustomerForm/CustomerFloatingActions.js b/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
index 2db9b862b..58d0a5f27 100644
--- a/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
+++ b/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
@@ -9,23 +9,24 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
-import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
-import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik';
+import styled from 'styled-components';
+
+import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { Icon } from 'components';
import { useCustomerFormContext } from './CustomerFormProvider';
+import { safeInvoke } from 'utils';
+
/**
* Customer floating actions bar.
*/
-export default function CustomerFloatingActions() {
- const history = useHistory();
-
+export default function CustomerFloatingActions({ onCancel }) {
// Customer form context.
- const { isNewMode,setSubmitPayload } = useCustomerFormContext();
+ const { isNewMode, setSubmitPayload } = useCustomerFormContext();
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
@@ -37,7 +38,7 @@ export default function CustomerFloatingActions() {
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
- history.goBack();
+ safeInvoke(onCancel, event);
};
// handle clear button clicl.
@@ -55,7 +56,7 @@ export default function CustomerFloatingActions() {
{/* ----------- Save and New ----------- */}
-
+
{/* ----------- Clear & Reset----------- */}
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
diff --git a/src/containers/Customers/CustomerForm/CustomerForm.js b/src/containers/Customers/CustomerForm/CustomerForm.js
index c8648bfd0..7adf26da5 100644
--- a/src/containers/Customers/CustomerForm/CustomerForm.js
+++ b/src/containers/Customers/CustomerForm/CustomerForm.js
@@ -1,152 +1,14 @@
-import React, { useMemo } from 'react';
-import { Formik, Form } from 'formik';
-import moment from 'moment';
-import { Intent } from '@blueprintjs/core';
-import intl from 'react-intl-universal';
-import classNames from 'classnames';
-import { useHistory } from 'react-router-dom';
-
-import { CLASSES } from 'common/classes';
-import AppToaster from 'components/AppToaster';
-
-import CustomerFormPrimarySection from './CustomerFormPrimarySection';
-import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
-import CustomersTabs from './CustomersTabs';
-import CustomerFloatingActions from './CustomerFloatingActions';
-
-import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
-
-import { compose, transformToForm } from 'utils';
-import { useCustomerFormContext } from './CustomerFormProvider';
-import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
-
-const defaultInitialValues = {
- customer_type: 'business',
- salutation: '',
- first_name: '',
- last_name: '',
- company_name: '',
- display_name: '',
-
- email: '',
- work_phone: '',
- personal_phone: '',
- website: '',
- note: '',
- active: true,
-
- billing_address_country: '',
- billing_address_1: '',
- billing_address_2: '',
- billing_address_city: '',
- billing_address_state: '',
- billing_address_postcode: '',
- billing_address_phone: '',
-
- shipping_address_country: '',
- shipping_address_1: '',
- shipping_address_2: '',
- shipping_address_city: '',
- shipping_address_state: '',
- shipping_address_postcode: '',
- shipping_address_phone: '',
-
- opening_balance: '',
- currency_code: '',
- opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
-};
+import React from 'react';
+import { CustomerFormProvider } from './CustomerFormProvider';
+import CustomerFormFormik from './CustomerFormFormik';
/**
- * Customer form.
+ * Abstructed customer form.
*/
-function CustomerForm({ organization: { base_currency } }) {
- const {
- customer,
- customerId,
- submitPayload,
- contactDuplicate,
- editCustomerMutate,
- createCustomerMutate,
- isNewMode,
- } = useCustomerFormContext();
-
- // const isNewMode = !customerId;
- const history = useHistory();
-
- /**
- * Initial values in create and edit mode.
- */
- const initialValues = useMemo(
- () => ({
- ...defaultInitialValues,
- currency_code: base_currency,
- ...transformToForm(contactDuplicate || customer, defaultInitialValues),
- }),
- [customer, contactDuplicate, base_currency],
- );
-
- //Handles the form submit.
- const handleFormSubmit = (
- values,
- { setSubmitting, resetForm, setErrors },
- ) => {
- const formValues = { ...values };
-
- const onSuccess = () => {
- AppToaster.show({
- message: intl.get(
- isNewMode
- ? 'the_customer_has_been_created_successfully'
- : 'the_item_customer_has_been_edited_successfully',
- ),
- intent: Intent.SUCCESS,
- });
- setSubmitting(false);
- resetForm();
-
- if (!submitPayload.noRedirect) {
- history.push('/customers');
- }
- };
-
- const onError = () => {
- setSubmitting(false);
- };
-
- if (isNewMode) {
- createCustomerMutate(formValues).then(onSuccess).catch(onError);
- } else {
- editCustomerMutate([customer.id, formValues])
- .then(onSuccess)
- .catch(onError);
- }
- };
-
+export default function CustomerForm({ customerId }) {
return (
-
+
+
+
);
}
-
-export default compose(withCurrentOrganization())(CustomerForm);
diff --git a/src/containers/Customers/CustomerForm/CustomerFormFormik.js b/src/containers/Customers/CustomerForm/CustomerFormFormik.js
new file mode 100644
index 000000000..ff5851d20
--- /dev/null
+++ b/src/containers/Customers/CustomerForm/CustomerFormFormik.js
@@ -0,0 +1,127 @@
+import React, { useMemo } from 'react';
+import { Formik, Form } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import intl from 'react-intl-universal';
+import classNames from 'classnames';
+
+import { CLASSES } from 'common/classes';
+import AppToaster from 'components/AppToaster';
+
+import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
+
+import CustomerFormPrimarySection from './CustomerFormPrimarySection';
+import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
+import CustomersTabs from './CustomersTabs';
+import CustomerFloatingActions from './CustomerFloatingActions';
+
+import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
+
+import { compose, transformToForm, saveInvoke } from 'utils';
+import { useCustomerFormContext } from './CustomerFormProvider';
+import { defaultInitialValues } from './utils';
+
+import 'style/pages/Customers/Form.scss';
+
+/**
+ * Customer form.
+ */
+function CustomerFormFormik({
+ organization: { base_currency },
+
+ // #ownProps
+ initialValues: initialCustomerValues,
+ onSubmitSuccess,
+ onSubmitError,
+ onCancel,
+ className,
+}) {
+ const {
+ customer,
+ submitPayload,
+ contactDuplicate,
+ editCustomerMutate,
+ createCustomerMutate,
+ isNewMode,
+ } = useCustomerFormContext();
+
+ /**
+ * Initial values in create and edit mode.
+ */
+ const initialValues = useMemo(
+ () => ({
+ ...defaultInitialValues,
+ currency_code: base_currency,
+ ...transformToForm(contactDuplicate || customer, defaultInitialValues),
+ ...initialCustomerValues,
+ }),
+ [customer, contactDuplicate, base_currency, initialCustomerValues],
+ );
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, formArgs) => {
+ const { setSubmitting, resetForm } = formArgs;
+ const formValues = { ...values };
+
+ const onSuccess = () => {
+ AppToaster.show({
+ message: intl.get(
+ isNewMode
+ ? 'the_customer_has_been_created_successfully'
+ : 'the_item_customer_has_been_edited_successfully',
+ ),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ resetForm();
+
+ saveInvoke(onSubmitSuccess, values, formArgs, submitPayload);
+ };
+
+ const onError = () => {
+ setSubmitting(false);
+ saveInvoke(onSubmitError, values, formArgs, submitPayload);
+ };
+
+ if (isNewMode) {
+ createCustomerMutate(formValues).then(onSuccess).catch(onError);
+ } else {
+ editCustomerMutate([customer.id, formValues])
+ .then(onSuccess)
+ .catch(onError);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withCurrentOrganization())(CustomerFormFormik);
diff --git a/src/containers/Customers/CustomerForm/CustomerFormPage.js b/src/containers/Customers/CustomerForm/CustomerFormPage.js
index 50d264d1b..5cf1d3971 100644
--- a/src/containers/Customers/CustomerForm/CustomerFormPage.js
+++ b/src/containers/Customers/CustomerForm/CustomerFormPage.js
@@ -1,20 +1,74 @@
import React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams, useHistory } from 'react-router-dom';
+import styled from 'styled-components';
import { DashboardCard } from 'components';
-import CustomerForm from './CustomerForm';
-import { CustomerFormProvider } from './CustomerFormProvider';
+import DashboardInsider from 'components/Dashboard/DashboardInsider';
-import 'style/pages/Customers/PageForm.scss';
+import CustomerFormFormik from './CustomerFormFormik';
+import {
+ CustomerFormProvider,
+ useCustomerFormContext,
+} from './CustomerFormProvider';
-export default function CustomerFormPage() {
- const { id } = useParams();
+/**
+ * Customer form page loading.
+ * @returns {JSX}
+ */
+function CustomerFormPageLoading({ children }) {
+ const { isFormLoading } = useCustomerFormContext();
return (
-
-
-
-
+
+ {children}
+
+ );
+}
+
+/**
+ * Customer form page.
+ * @returns {JSX}
+ */
+export default function CustomerFormPage() {
+ const history = useHistory();
+ const { id } = useParams();
+
+ const customerId = parseInt(id, 10);
+
+ // Handle the form submit success.
+ const handleSubmitSuccess = (values, formArgs, submitPayload) => {
+ if (!submitPayload.noRedirect) {
+ history.push('/customers');
+ }
+ };
+ // Handle the form cancel button click.
+ const handleFormCancel = () => {
+ history.goBack();
+ };
+
+ return (
+
+
+
+
+
+
);
-}
\ No newline at end of file
+}
+
+const CustomerFormPageFormik = styled(CustomerFormFormik)`
+ .page-form {
+ &__floating-actions {
+ margin-left: -40px;
+ margin-right: -40px;
+ }
+ }
+`;
+
+const CustomerDashboardInsider = styled(DashboardInsider)`
+ padding-bottom: 64px;
+`;
diff --git a/src/containers/Customers/CustomerForm/CustomerFormProvider.js b/src/containers/Customers/CustomerForm/CustomerFormProvider.js
index 64fa78685..ae678b4b8 100644
--- a/src/containers/Customers/CustomerForm/CustomerFormProvider.js
+++ b/src/containers/Customers/CustomerForm/CustomerFormProvider.js
@@ -1,6 +1,5 @@
import React, { useState, createContext } from 'react';
import { useLocation } from 'react-router-dom';
-import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useCustomer,
useCurrencies,
@@ -24,7 +23,7 @@ function CustomerFormProvider({ customerId, ...props }) {
// Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId,
- { enabled: !!contactId, },
+ { enabled: !!contactId },
);
// Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
@@ -38,6 +37,9 @@ function CustomerFormProvider({ customerId, ...props }) {
// determines whether the form new or duplicate mode.
const isNewMode = contactId || !customerId;
+ const isFormLoading =
+ isCustomerLoading || isCurrenciesLoading || isContactLoading;
+
const provider = {
customerId,
customer,
@@ -48,24 +50,14 @@ function CustomerFormProvider({ customerId, ...props }) {
isCustomerLoading,
isCurrenciesLoading,
+ isFormLoading,
setSubmitPayload,
editCustomerMutate,
createCustomerMutate,
};
- return (
-
-
-
- );
+ return ;
}
const useCustomerFormContext = () => React.useContext(CustomerFormContext);
diff --git a/src/containers/Customers/CustomerForm/utils.js b/src/containers/Customers/CustomerForm/utils.js
new file mode 100644
index 000000000..d72cfb92d
--- /dev/null
+++ b/src/containers/Customers/CustomerForm/utils.js
@@ -0,0 +1,38 @@
+import moment from 'moment';
+
+
+export const defaultInitialValues = {
+ customer_type: 'business',
+ salutation: '',
+ first_name: '',
+ last_name: '',
+ company_name: '',
+ display_name: '',
+
+ email: '',
+ work_phone: '',
+ personal_phone: '',
+ website: '',
+ note: '',
+ active: true,
+
+ billing_address_country: '',
+ billing_address_1: '',
+ billing_address_2: '',
+ billing_address_city: '',
+ billing_address_state: '',
+ billing_address_postcode: '',
+ billing_address_phone: '',
+
+ shipping_address_country: '',
+ shipping_address_1: '',
+ shipping_address_2: '',
+ shipping_address_city: '',
+ shipping_address_state: '',
+ shipping_address_postcode: '',
+ shipping_address_phone: '',
+
+ opening_balance: '',
+ currency_code: '',
+ opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
+};
diff --git a/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js b/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
index 967fb68e0..852dca76e 100644
--- a/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
+++ b/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
@@ -37,7 +37,7 @@ export default function QuickPaymentMadeFormFields() {
const { accounts } = useQuickPaymentMadeContext();
// Intl context.
-
+
const paymentMadeFieldRef = useAutofocus();
return (
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js
new file mode 100644
index 000000000..260b5ef15
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {
+ DrawerHeaderContent,
+ DrawerBody,
+ FormattedMessage as T,
+} from 'components';
+
+import QuickCustomerFormDrawer from './QuickCustomerFormDrawer';
+
+/**
+ * Quick create/edit customer drawer.
+ */
+export default function QuickCreateCustomerDrawerContent({ displayName }) {
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js
new file mode 100644
index 000000000..1b665c384
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import * as R from 'ramda';
+import styled from 'styled-components';
+
+import { Card, DrawerLoading } from 'components';
+
+import {
+ CustomerFormProvider,
+ useCustomerFormContext,
+} from '../../Customers/CustomerForm/CustomerFormProvider';
+import CustomerFormFormik from '../../Customers/CustomerForm/CustomerFormFormik';
+
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+
+/**
+ * Drawer customer form loading wrapper.
+ * @returns {JSX}
+ */
+function DrawerCustomerFormLoading({ children }) {
+ const { isFormLoading } = useCustomerFormContext();
+
+ return {children};
+}
+
+/**
+ * Quick customer form of the drawer.
+ */
+function QuickCustomerFormDrawer({ displayName, closeDrawer, customerId }) {
+ // Handle the form submit request success.
+ const handleSubmitSuccess = () => {
+ closeDrawer('quick-create-customer');
+ };
+ // Handle the form cancel action.
+ const handleCancelForm = () => {
+ closeDrawer('quick-create-customer');
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default R.compose(withDrawerActions)(QuickCustomerFormDrawer);
+
+const CustomerFormCard = styled(Card)`
+ margin: 15px;
+ margin-bottom: calc(15px + 65px);
+
+ .page-form {
+ &__floating-actions {
+ margin-left: -36px;
+ margin-right: -36px;
+ }
+ }
+`;
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/index.js b/src/containers/Drawers/QuickCreateCustomerDrawer/index.js
new file mode 100644
index 000000000..ce970532e
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/index.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Drawer, DrawerSuspense } from 'components';
+import withDrawers from 'containers/Drawer/withDrawers';
+
+import { compose } from 'utils';
+
+const QuickCreateCustomerDrawerContent = React.lazy(() =>
+ import('./QuickCreateCustomerDrawerContent'),
+);
+
+/**
+ * Quick Create customer
+ */
+function QuickCreateCustomerDrawer({
+ name,
+
+ // #withDrawer
+ isOpen,
+ payload,
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(withDrawers())(QuickCreateCustomerDrawer);
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js
new file mode 100644
index 000000000..b470822f8
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {
+ DrawerHeaderContent,
+ DrawerBody,
+ FormattedMessage as T,
+} from 'components';
+
+import QuickCreateItemDrawerForm from './QuickCreateItemDrawerForm';
+
+/**
+ * Quick create/edit item drawer content.
+ */
+export default function QuickCreateItemDrawerContent({ itemName }) {
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js
new file mode 100644
index 000000000..ada5848bb
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import * as R from 'ramda';
+import styled from 'styled-components';
+
+import { Card, DrawerLoading } from 'components';
+
+import ItemFormFormik from '../../Items/ItemFormFormik';
+import {
+ ItemFormProvider,
+ useItemFormContext,
+} from '../../Items/ItemFormProvider';
+
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+import withDashboardActions from '../../Dashboard/withDashboardActions';
+
+import { useDrawerContext } from 'components/Drawer/DrawerProvider';
+
+/**
+ * Drawer item form loading.
+ * @returns {JSX}
+ */
+function DrawerItemFormLoading({ children }) {
+ const { isFormLoading } = useItemFormContext();
+
+ return {children};
+}
+
+/**
+ * Quick create/edit item drawer form.
+ */
+function QuickCreateItemDrawerForm({
+ itemId,
+ itemName,
+ closeDrawer,
+
+ // #withDashboardActions
+ addQuickActionEvent,
+}) {
+ // Drawer context.
+ const { payload } = useDrawerContext();
+
+ // Handle the form submit request success.
+ const handleSubmitSuccess = (values, form, submitPayload, response) => {
+ if (submitPayload.redirect) {
+ closeDrawer('quick-create-item');
+ }
+ if (payload.quickActionEvent) {
+ addQuickActionEvent(payload.quickActionEvent, {
+ itemId: response.data.id,
+ });
+ }
+ };
+ // Handle the form cancel.
+ const handleFormCancel = () => {
+ closeDrawer('quick-create-item');
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default R.compose(
+ withDrawerActions,
+ withDashboardActions,
+)(QuickCreateItemDrawerForm);
+
+const ItemFormCard = styled(Card)`
+ margin: 15px;
+ margin-bottom: calc(15px + 65px);
+
+ .page-form__floating-actions {
+ margin-left: -36px;
+ margin-right: -36px;
+ }
+`;
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/index.js b/src/containers/Drawers/QuickCreateItemDrawer/index.js
new file mode 100644
index 000000000..972288d2a
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Drawer, DrawerSuspense } from 'components';
+import withDrawers from 'containers/Drawer/withDrawers';
+
+import { compose } from 'utils';
+
+const QuickCretaeItemDrawerContent = React.lazy(() =>
+ import('./QuickCreateItemDrawerContent'),
+);
+
+/**
+ * Quick create item.
+ */
+function QuickCreateItemDrawer({
+ // #ownProps
+ name,
+
+ // #withDrawer
+ isOpen,
+ payload,
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(withDrawers())(QuickCreateItemDrawer);
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js b/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js
new file mode 100644
index 000000000..6996dac2a
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import * as R from 'ramda';
+import styled from 'styled-components';
+
+import { Card, DrawerLoading } from 'components';
+
+import {
+ VendorFormProvider,
+ useVendorFormContext,
+} from '../../Vendors/VendorForm/VendorFormProvider';
+import VendorFormFormik from '../../Vendors/VendorForm/VendorFormFormik';
+
+import withDrawerActions from 'containers/Drawer/withDrawerActions';
+import withDashboardActions from '../../Dashboard/withDashboardActions';
+
+import { useDrawerContext } from 'components/Drawer/DrawerProvider';
+
+/**
+ * Drawer vendor form loading wrapper.
+ * @returns {JSX}
+ */
+function DrawerVendorFormLoading({ children }) {
+ const { isFormLoading } = useVendorFormContext();
+
+ return {children};
+}
+
+/**
+ * Quick vendor form of the drawer.
+ */
+function QuickVendorFormDrawer({
+ displayName,
+ closeDrawer,
+ vendorId,
+ addQuickActionEvent,
+}) {
+ const { payload } = useDrawerContext();
+
+ // Handle the form submit request success.
+ const handleSubmitSuccess = (values, form, submitPayload, response) => {
+ if (!submitPayload.noRedirect) {
+ closeDrawer('quick-write-vendor');
+ }
+ if (payload.quickActionEvent) {
+ addQuickActionEvent(payload.quickActionEvent, {
+ vendorId: response.data.id,
+ });
+ }
+ };
+ // Handle the form cancel action.
+ const handleCancelForm = () => {
+ closeDrawer('quick-write-vendor');
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default R.compose(
+ withDrawerActions,
+ withDashboardActions,
+)(QuickVendorFormDrawer);
+
+const VendorFormCard = styled(Card)`
+ margin: 15px;
+ margin-bottom: calc(15px + 65px);
+
+ .page-form {
+ &__floating-actions {
+ margin-left: -36px;
+ margin-right: -36px;
+ }
+ }
+`;
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js b/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js
new file mode 100644
index 000000000..b686d2211
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {
+ DrawerHeaderContent,
+ DrawerBody,
+ FormattedMessage as T,
+} from 'components';
+
+import QuickVendorFormDrawer from './QuickVendorFormDrawer';
+
+/**
+ * Quick create/edit vendor drawer.
+ */
+export default function QuickWriteVendorDrawerContent({ displayName }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/index.js b/src/containers/Drawers/QuickWriteVendorDrawer/index.js
new file mode 100644
index 000000000..6bd8ea2a4
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/index.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import * as R from 'ramda';
+
+import { Drawer, DrawerSuspense } from 'components';
+import withDrawers from 'containers/Drawer/withDrawers';
+
+const QuickWriteVendorDrawerContent = React.lazy(() =>
+ import('./QuickWriteVendorDrawerContent'),
+);
+
+/**
+ * Quick Write vendor.
+ */
+function QuickWriteVendorDrawer({
+ name,
+
+ // #withDrawer
+ isOpen,
+ payload,
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+export default R.compose(withDrawers())(QuickWriteVendorDrawer);
diff --git a/src/containers/Entries/components.js b/src/containers/Entries/components.js
index 85c057e04..80fcce6e3 100644
--- a/src/containers/Entries/components.js
+++ b/src/containers/Entries/components.js
@@ -108,7 +108,9 @@ const LandedCostHeaderCell = () => {
/**
* Retrieve editable items entries columns.
*/
-export function useEditableItemsEntriesColumns({ landedCost }) {
+export function useEditableItemsEntriesColumns({
+ landedCost,
+}) {
return React.useMemo(
() => [
{
@@ -129,6 +131,7 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
disableSortBy: true,
width: 130,
className: 'item',
+ fieldProps: { allowCreate: true },
},
{
Header: intl.get('description'),
diff --git a/src/containers/Entries/utils.js b/src/containers/Entries/utils.js
index 7bfde9d47..22d8adb54 100644
--- a/src/containers/Entries/utils.js
+++ b/src/containers/Entries/utils.js
@@ -164,3 +164,12 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
updateTableRow(rowIndex, newRow),
)(rows);
});
+
+/**
+ *
+ * @param {*} entries
+ * @returns
+ */
+export const composeControlledEntries = (entries) => {
+ return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
+};
diff --git a/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js b/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
index 407abb290..5208759fc 100644
--- a/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
+++ b/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
@@ -14,7 +14,7 @@ import {
import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils';
import {
CurrencySelectList,
- ContactSelecetList,
+ CustomerSelectField,
AccountsSelectList,
FieldRequiredHint,
Hint,
@@ -78,6 +78,7 @@ export default function ExpenseFormHeader() {
defaultSelectText={}
selectedAccountId={value}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
+ allowCreate={true}
/>
)}
@@ -137,13 +138,14 @@ export default function ExpenseFormHeader() {
helperText={}
inline={true}
>
- }
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Expenses/ExpenseForm/components.js b/src/containers/Expenses/ExpenseForm/components.js
index 64f275d00..fb52ac569 100644
--- a/src/containers/Expenses/ExpenseForm/components.js
+++ b/src/containers/Expenses/ExpenseForm/components.js
@@ -113,6 +113,7 @@ export function useExpenseFormTableColumns({ landedCost }) {
disableSortBy: true,
width: 40,
filterAccountsByRootTypes: ['expense'],
+ fieldProps: { allowCreate: true },
},
{
Header: ExpenseAmountHeaderCell,
diff --git a/src/containers/Items/ItemForm.js b/src/containers/Items/ItemForm.js
index 99847d298..6638fe57d 100644
--- a/src/containers/Items/ItemForm.js
+++ b/src/containers/Items/ItemForm.js
@@ -1,110 +1,95 @@
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 { useHistory } from 'react-router-dom';
+import styled from 'styled-components';
-import 'style/pages/Items/PageForm.scss';
+import { useDashboardPageTitle } from 'hooks/state';
+import { useItemFormContext, ItemFormProvider } from './ItemFormProvider';
-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 ItemFormFormik from './ItemFormFormik';
-import {
- transformSubmitRequestErrors,
- useItemFormInitialValues,
-} from './utils';
-import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
-
-import { useItemFormContext } from './ItemFormProvider';
+import DashboardCard from 'components/Dashboard/DashboardCard';
+import DashboardInsider from 'components/Dashboard/DashboardInsider';
/**
- * Item form.
+ * Item form dashboard title.
+ * @returns {null}
*/
-export default function ItemForm() {
- // Item form context.
- const {
- itemId,
- item,
- accounts,
- createItemMutate,
- editItemMutate,
- submitPayload,
- isNewMode,
- } = useItemFormContext();
+function ItemFormDashboardTitle() {
+ // Change page title dispatcher.
+ const changePageTitle = useDashboardPageTitle();
+ // Item form context.
+ const { isNewMode } = useItemFormContext();
+
+ // Changes the page title in new and edit mode.
+ React.useEffect(() => {
+ isNewMode
+ ? changePageTitle(intl.get('new_item'))
+ : changePageTitle(intl.get('edit_item_details'));
+ }, [changePageTitle, isNewMode]);
+
+ return null;
+}
+
+/**
+ * Item form page loading state indicator.
+ * @returns {JSX}
+ */
+function ItemFormPageLoading({ children }) {
+ const { isFormLoading } = useItemFormContext();
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Item form of the page.
+ * @returns {JSX}
+ */
+export default function ItemForm({ itemId }) {
// History context.
const history = useHistory();
- // Initial values in create and edit mode.
- const initialValues = useItemFormInitialValues(item);
-
- // 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 = transformSubmitRequestErrors(errors);
- setErrors({ ..._errors });
- }
- };
- if (isNewMode) {
- createItemMutate(form).then(onSuccess).catch(onError);
- } else {
- editItemMutate([itemId, form]).then(onSuccess).catch(onError);
+ // Handle the form submit success.
+ const handleSubmitSuccess = (values, form, submitPayload) => {
+ if (submitPayload.redirect) {
+ 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/src/containers/Items/ItemFormBody.js b/src/containers/Items/ItemFormBody.js
index 5305e8ba2..2a7e06a19 100644
--- a/src/containers/Items/ItemFormBody.js
+++ b/src/containers/Items/ItemFormBody.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { useFormikContext, FastField, Field, ErrorMessage } from 'formik';
+import { useFormikContext, FastField, ErrorMessage } from 'formik';
import {
FormGroup,
Classes,
@@ -122,6 +122,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true}
+ allowCreate={true}
/>
)}
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Items/ItemFormFloatingActions.js b/src/containers/Items/ItemFormFloatingActions.js
index eacce65db..fbdb1a152 100644
--- a/src/containers/Items/ItemFormFloatingActions.js
+++ b/src/containers/Items/ItemFormFloatingActions.js
@@ -1,20 +1,19 @@
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 styled from 'styled-components';
import { FastField, useFormikContext } from 'formik';
+import classNames from 'classnames';
+
+import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider';
+import { saveInvoke } from '../../utils';
/**
* Item form floating actions.
*/
-export default function ItemFormFloatingActions() {
- // History context.
- const history = useHistory();
-
+export default function ItemFormFloatingActions({ onCancel }) {
// Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext();
@@ -23,7 +22,7 @@ export default function ItemFormFloatingActions() {
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
- history.goBack();
+ saveInvoke(onCancel, event);
};
// Handle submit button click.
@@ -38,7 +37,7 @@ export default function ItemFormFloatingActions() {
return (
-
{isNewMode ? : }
-
+
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
\ No newline at end of file
diff --git a/src/containers/Items/ItemFormFormik.js b/src/containers/Items/ItemFormFormik.js
new file mode 100644
index 000000000..7876cd6ed
--- /dev/null
+++ b/src/containers/Items/ItemFormFormik.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import { Formik, Form } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import intl from 'react-intl-universal';
+import classNames from 'classnames';
+
+import 'style/pages/Items/Form.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 {
+ transformSubmitRequestErrors,
+ useItemFormInitialValues,
+} from './utils';
+import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
+
+import { useItemFormContext } from './ItemFormProvider';
+import { safeInvoke } from '@blueprintjs/core/lib/esm/common/utils';
+
+/**
+ * Item form.
+ */
+export default function ItemFormFormik({
+ // #ownProps
+ initialValues: initialValuesComponent,
+ onSubmitSuccess,
+ onSubmitError,
+ onCancel,
+ className,
+}) {
+ // Item form context.
+ const {
+ itemId,
+ item,
+ accounts,
+ createItemMutate,
+ editItemMutate,
+ submitPayload,
+ isNewMode,
+ } = useItemFormContext();
+
+ // Initial values in create and edit mode.
+ const initialValues = useItemFormInitialValues(item, initialValuesComponent);
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, form) => {
+ const { setSubmitting, resetForm, setErrors } = form;
+ const formValues = { ...values };
+
+ setSubmitting(true);
+
+ // Handle response succes.
+ 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);
+
+ safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
+ };
+ // Handle response error.
+ const onError = (errors) => {
+ setSubmitting(false);
+
+ if (errors) {
+ const _errors = transformSubmitRequestErrors(errors);
+ setErrors({ ..._errors });
+ }
+ safeInvoke(onSubmitError, values, form, submitPayload, errors);
+ };
+ if (isNewMode) {
+ createItemMutate(formValues).then(onSuccess).catch(onError);
+ } else {
+ editItemMutate([itemId, formValues]).then(onSuccess).catch(onError);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/containers/Items/ItemFormPage.js b/src/containers/Items/ItemFormPage.js
index 5852b1bdd..c57f03661 100644
--- a/src/containers/Items/ItemFormPage.js
+++ b/src/containers/Items/ItemFormPage.js
@@ -1,9 +1,7 @@
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';
+import ItemForm from './ItemForm';
/**
* Item form page.
@@ -12,11 +10,5 @@ export default function ItemFormPage() {
const { id } = useParams();
const idInteger = parseInt(id, 10);
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
+ return ;
+}
diff --git a/src/containers/Items/ItemFormProvider.js b/src/containers/Items/ItemFormProvider.js
index b491a2d5b..85dcd7276 100644
--- a/src/containers/Items/ItemFormProvider.js
+++ b/src/containers/Items/ItemFormProvider.js
@@ -1,8 +1,5 @@
-import React, { useEffect, createContext, useState } from 'react';
-import intl from 'react-intl-universal';
+import React, { createContext, useState } from 'react';
import { useLocation } from 'react-router-dom';
-
-import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useItem,
useSettingsItems,
@@ -11,7 +8,6 @@ import {
useEditItem,
useAccounts,
} from 'hooks/query';
-import { useDashboardPageTitle } from 'hooks/state';
import { useWatchItemError } from './utils';
const ItemFormContext = createContext();
@@ -59,6 +55,13 @@ function ItemFormProvider({ itemId, ...props }) {
// Detarmines whether the form new mode.
const isNewMode = duplicateId || !itemId;
+ // Detarmines the form loading state.
+ const isFormLoading =
+ isItemsSettingsLoading ||
+ isAccountsLoading ||
+ isItemsCategoriesLoading ||
+ isItemLoading;
+
// Provider state.
const provider = {
itemId,
@@ -68,6 +71,7 @@ function ItemFormProvider({ itemId, ...props }) {
submitPayload,
isNewMode,
+ isFormLoading,
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
@@ -77,27 +81,7 @@ function ItemFormProvider({ itemId, ...props }) {
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 (
-
-
-
- );
+ return ;
}
const useItemFormContext = () => React.useContext(ItemFormContext);
diff --git a/src/containers/Items/utils.js b/src/containers/Items/utils.js
index 5d5dbf156..406746786 100644
--- a/src/containers/Items/utils.js
+++ b/src/containers/Items/utils.js
@@ -33,7 +33,7 @@ const defaultInitialValues = {
/**
* Initial values in create and edit mode.
*/
-export const useItemFormInitialValues = (item) => {
+export const useItemFormInitialValues = (item, initialValues) => {
const { items: itemsSettings } = useSettingsSelector();
return useMemo(
@@ -54,8 +54,9 @@ export const useItemFormInitialValues = (item) => {
transformItemFormData(item, defaultInitialValues),
defaultInitialValues,
),
+ ...initialValues,
}),
- [item, itemsSettings],
+ [item, itemsSettings, initialValues],
);
};
diff --git a/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js b/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
index dfa2a5d28..44bd7d75e 100644
--- a/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
+++ b/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
@@ -6,7 +6,7 @@ import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
-import { ContactSelecetList, FieldRequiredHint, Icon } from 'components';
+import { VendorSelectField, FieldRequiredHint, Icon } from 'components';
import { vendorsFieldShouldUpdate } from './utils';
import { useBillFormContext } from './BillFormProvider';
@@ -43,14 +43,15 @@ function BillFormHeader() {
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js b/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
index d59d8994c..d2177f3ed 100644
--- a/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
+++ b/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
@@ -15,7 +15,7 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
- ContactSelecetList,
+ VendorSelectField,
FieldRequiredHint,
InputPrependText,
Money,
@@ -90,8 +90,8 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
@@ -100,6 +100,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
}}
disabled={!isNewMode}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js b/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
index fce425f3a..e83642bf8 100644
--- a/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
+++ b/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
@@ -19,7 +19,7 @@ import { customersFieldShouldUpdate } from './utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -82,8 +82,8 @@ function EstimateFormHeader({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
@@ -91,6 +91,7 @@ function EstimateFormHeader({
}}
popoverFill={true}
intent={inputIntent({ error, touched })}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js b/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
index 1775d8d78..d5c8aaca6 100644
--- a/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
+++ b/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
@@ -10,20 +10,23 @@ import { FastField, Field, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames';
+
import {
useObserveInvoiceNoSettings,
customerNameFieldShouldUpdate,
} from './utils';
import { CLASSES } from 'common/classes';
import {
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import { useInvoiceFormContext } from './InvoiceFormProvider';
+
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
+
import { inputIntent, handleDateChange } from 'utils';
/**
@@ -84,14 +87,15 @@ function InvoiceFormHeaderFields({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js b/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
index 452ca971d..743e97d32 100644
--- a/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
+++ b/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
@@ -23,7 +23,7 @@ import {
} from 'utils';
import {
AccountsSelectList,
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -134,8 +134,8 @@ function PaymentReceiveHeaderFields({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
@@ -147,6 +147,7 @@ function PaymentReceiveHeaderFields({
buttonProps={{
elementRef: (ref) => (customerFieldRef.current = ref),
}}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js b/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
index 60ec206bb..9f384dd77 100644
--- a/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
+++ b/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
@@ -12,7 +12,7 @@ import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -88,14 +88,15 @@ function ReceiptFormHeader({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
@@ -129,6 +130,7 @@ function ReceiptFormHeader({
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Vendors/VendorForm/VendorFloatingActions.js b/src/containers/Vendors/VendorForm/VendorFloatingActions.js
index 1da3670da..085efd598 100644
--- a/src/containers/Vendors/VendorForm/VendorFloatingActions.js
+++ b/src/containers/Vendors/VendorForm/VendorFloatingActions.js
@@ -9,42 +9,43 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
-import { FormattedMessage as T } from 'components';
+import styled from 'styled-components';
import classNames from 'classnames';
-import { CLASSES } from 'common/classes';
import { useFormikContext } from 'formik';
-import { useHistory } from 'react-router-dom';
+
+import { FormattedMessage as T } from 'components';
+
+import { CLASSES } from 'common/classes';
import { Icon } from 'components';
import { useVendorFormContext } from './VendorFormProvider';
+import { safeInvoke } from 'utils';
+
/**
* Vendor floating actions bar.
*/
-export default function VendorFloatingActions() {
+export default function VendorFloatingActions({ onCancel }) {
// Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext();
// Vendor form context.
const { isNewMode, setSubmitPayload } = useVendorFormContext();
- // History.
- const history = useHistory();
-
// Handle the submit button.
const handleSubmitBtnClick = (event) => {
- setSubmitPayload({ noRedirect: false, });
+ setSubmitPayload({ noRedirect: false });
submitForm();
};
// Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => {
submitForm();
- setSubmitPayload({ noRedirect: true, });
+ setSubmitPayload({ noRedirect: true });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
- history.goBack();
+ safeInvoke(onCancel, event);
};
// Handle clear button click.
@@ -56,7 +57,7 @@ export default function VendorFloatingActions() {
{/* ----------- Save and New ----------- */}
-
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
diff --git a/src/containers/Vendors/VendorForm/VendorForm.js b/src/containers/Vendors/VendorForm/VendorFormFormik.js
similarity index 63%
rename from src/containers/Vendors/VendorForm/VendorForm.js
rename to src/containers/Vendors/VendorForm/VendorFormFormik.js
index db9474aa1..c45bb2e59 100644
--- a/src/containers/Vendors/VendorForm/VendorForm.js
+++ b/src/containers/Vendors/VendorForm/VendorFormFormik.js
@@ -1,13 +1,10 @@
-import React, { useMemo, useEffect } from 'react';
+import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
-import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
-import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
-import { FormattedMessage as T } from 'components';
import AppToaster from 'components/AppToaster';
import {
CreateVendorFormSchema,
@@ -19,56 +16,27 @@ import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection';
import VendorTabs from './VendorsTabs';
import VendorFloatingActions from './VendorFloatingActions';
-import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { useVendorFormContext } from './VendorFormProvider';
-import { compose, transformToForm } from 'utils';
+import { compose, transformToForm, safeInvoke } from 'utils';
-const defaultInitialValues = {
- salutation: '',
- first_name: '',
- last_name: '',
- company_name: '',
- display_name: '',
+import { defaultInitialValues } from './utils';
- email: '',
- work_phone: '',
- personal_phone: '',
- website: '',
- note: '',
- active: true,
-
- billing_address_country: '',
- billing_address_1: '',
- billing_address_2: '',
- billing_address_city: '',
- billing_address_state: '',
- billing_address_postcode: '',
- billing_address_phone: '',
-
- shipping_address_country: '',
- shipping_address_1: '',
- shipping_address_2: '',
- shipping_address_city: '',
- shipping_address_state: '',
- shipping_address_postcode: '',
- shipping_address_phone: '',
-
- opening_balance: '',
- currency_code: '',
- opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
-};
+import 'style/pages/Vendors/Form.scss';
/**
* Vendor form.
*/
-function VendorForm({
- // #withDashboardActions
- changePageTitle,
-
+function VendorFormFormik({
// #withCurrentOrganization
organization: { base_currency },
+
+ // #ownProps
+ onSubmitSuccess,
+ onSubmitError,
+ onCancel,
+ className,
}) {
// Vendor form context.
const {
@@ -82,11 +50,6 @@ function VendorForm({
isNewMode,
} = useVendorFormContext();
- // const isNewMode = !vendorId;
-
- // History context.
- const history = useHistory();
-
/**
* Initial values in create and edit mode.
*/
@@ -101,14 +64,13 @@ function VendorForm({
);
// Handles the form submit.
- const handleFormSubmit = (
- values,
- { setSubmitting, resetForm, setErrors },
- ) => {
+ const handleFormSubmit = (values, form) => {
+ const { setSubmitting, resetForm } = form;
const requestForm = { ...values };
+
setSubmitting(true);
- const onSuccess = () => {
+ const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
@@ -121,16 +83,15 @@ function VendorForm({
setSubmitting(false);
resetForm();
- if (!submitPayload.noRedirect) {
- history.push('/vendors');
- }
+ safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
};
const onError = () => {
setSubmitPayload(false);
setSubmitting(false);
- };
+ safeInvoke(onSubmitError, values, form, submitPayload);
+ };
if (isNewMode) {
createVendorMutate(requestForm).then(onSuccess).catch(onError);
} else {
@@ -139,7 +100,13 @@ function VendorForm({
};
return (
-
);
}
-export default compose(
- withDashboardActions,
- withCurrentOrganization(),
-)(VendorForm);
+export default compose(withCurrentOrganization())(VendorFormFormik);
diff --git a/src/containers/Vendors/VendorForm/VendorFormPage.js b/src/containers/Vendors/VendorForm/VendorFormPage.js
index 329644097..5553dba57 100644
--- a/src/containers/Vendors/VendorForm/VendorFormPage.js
+++ b/src/containers/Vendors/VendorForm/VendorFormPage.js
@@ -1,26 +1,69 @@
import React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams, useHistory } from 'react-router-dom';
+import styled from 'styled-components';
import 'style/pages/Vendors/PageForm.scss';
-import { DashboardCard } from 'components';
-import VendorFrom from './VendorForm';
+import { DashboardCard, DashboardInsider } from 'components';
+import VendorFormFormik from './VendorFormFormik';
-import { VendorFormProvider } from './VendorFormProvider';
+import { VendorFormProvider, useVendorFormContext } from './VendorFormProvider';
+
+/**
+ * Vendor form page loading wrapper.
+ * @returns {JSX}
+ */
+function VendorFormPageLoading({ children }) {
+ const { isFormLoading } = useVendorFormContext();
+
+ return (
+
+ {children}
+
+ );
+}
/**
* Vendor form page.
*/
-function VendorFormPage() {
+export default function VendorFormPage() {
+ const history = useHistory();
const { id } = useParams();
-
+
+ // Handle the form submit success.
+ const handleSubmitSuccess = (values, formArgs, submitPayload) => {
+ if (!submitPayload.noRedirect) {
+ history.push('/vendors');
+ }
+ };
+ // Handle the form cancel button click.
+ const handleFormCancel = () => {
+ history.goBack();
+ };
+
return (
-
-
-
+
+
+
+
+
);
}
-export default VendorFormPage;
+const VendorFormPageFormik = styled(VendorFormFormik)`
+ .page-form {
+ &__floating-actions {
+ margin-left: -40px;
+ margin-right: -40px;
+ }
+ }
+`;
+
+const VendorDashboardInsider = styled(DashboardInsider)`
+ padding-bottom: 64px;
+`;
diff --git a/src/containers/Vendors/VendorForm/VendorFormProvider.js b/src/containers/Vendors/VendorForm/VendorFormProvider.js
index 107b86af5..a40d2ed4f 100644
--- a/src/containers/Vendors/VendorForm/VendorFormProvider.js
+++ b/src/containers/Vendors/VendorForm/VendorFormProvider.js
@@ -6,7 +6,6 @@ import {
useVendor,
useContact,
useCurrencies,
- useCustomer,
useCreateVendor,
useEditVendor,
} from 'hooks/query';
@@ -30,11 +29,10 @@ function VendorFormProvider({ vendorId, ...props }) {
});
// Handle fetch contact duplicate details.
- const {
- data: contactDuplicate,
- isLoading: isContactLoading,
- } = useContact(contactId, { enabled: !!contactId });
-
+ const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
+ contactId,
+ { enabled: !!contactId },
+ );
// Create and edit vendor mutations.
const { mutateAsync: createVendorMutate } = useCreateVendor();
const { mutateAsync: editVendorMutate } = useEditVendor();
@@ -45,27 +43,25 @@ function VendorFormProvider({ vendorId, ...props }) {
// determines whether the form new or duplicate mode.
const isNewMode = contactId || !vendorId;
+ const isFormLoading =
+ isVendorLoading || isContactLoading || isCurrenciesLoading;
+
const provider = {
vendorId,
currencies,
vendor,
contactDuplicate: { ...omit(contactDuplicate, ['opening_balance_at']) },
submitPayload,
+
isNewMode,
+ isFormLoading,
createVendorMutate,
editVendorMutate,
setSubmitPayload,
};
- return (
-
-
-
- );
+ return ;
}
const useVendorFormContext = () => React.useContext(VendorFormContext);
diff --git a/src/containers/Vendors/VendorForm/utils.js b/src/containers/Vendors/VendorForm/utils.js
new file mode 100644
index 000000000..8c9a70509
--- /dev/null
+++ b/src/containers/Vendors/VendorForm/utils.js
@@ -0,0 +1,36 @@
+import moment from 'moment';
+
+export const defaultInitialValues = {
+ salutation: '',
+ first_name: '',
+ last_name: '',
+ company_name: '',
+ display_name: '',
+
+ email: '',
+ work_phone: '',
+ personal_phone: '',
+ website: '',
+ note: '',
+ active: true,
+
+ billing_address_country: '',
+ billing_address_1: '',
+ billing_address_2: '',
+ billing_address_city: '',
+ billing_address_state: '',
+ billing_address_postcode: '',
+ billing_address_phone: '',
+
+ shipping_address_country: '',
+ shipping_address_1: '',
+ shipping_address_2: '',
+ shipping_address_city: '',
+ shipping_address_state: '',
+ shipping_address_postcode: '',
+ shipping_address_phone: '',
+
+ opening_balance: '',
+ currency_code: '',
+ opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
+};
diff --git a/src/style/pages/Customers/Form.scss b/src/style/pages/Customers/Form.scss
new file mode 100644
index 000000000..6a90e84ed
--- /dev/null
+++ b/src/style/pages/Customers/Form.scss
@@ -0,0 +1,157 @@
+@import '../../Base.scss';
+
+.page-form--customer {
+ $self: '.page-form';
+ padding: 20px;
+
+ #{$self}__header {
+ padding: 0;
+ }
+ #{$self}__primary-section {
+ padding: 10px 0 0;
+ margin: 0 0 20px;
+ overflow: hidden;
+ border-bottom: 1px solid #e4e4e4;
+ max-width: 1000px;
+ }
+
+ .bp3-form-group {
+ max-width: 500px;
+
+ .bp3-control {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+
+ &.bp3-inline {
+ .bp3-label {
+ min-width: 150px;
+ }
+ }
+ .bp3-form-content {
+ width: 100%;
+ }
+ }
+
+ .form-group--contact_name {
+ max-width: 600px;
+
+ .bp3-control-group > * {
+ flex-shrink: unset;
+
+ &:not(:last-child) {
+ padding-right: 10px;
+ }
+ &.input-group--salutation-list {
+ width: 25%;
+ }
+ &.input-group--first-name,
+ &.input-group--last-name {
+ width: 37%;
+ }
+ }
+ }
+
+ .bp3-form-group {
+ margin-bottom: 14px;
+ }
+
+ .bp3-tab-panel {
+ margin-top: 26px;
+ }
+
+ .form-group--phone-number {
+ .bp3-control-group > * {
+ flex-shrink: unset;
+ padding-right: 5px;
+ padding-left: 5px;
+
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+ }
+
+ #{$self}__tabs {
+ margin-top: 20px;
+ max-width: 1000px;
+
+ h4 {
+ font-weight: 500;
+ color: #888;
+ margin-bottom: 1.2rem;
+ font-size: 14px;
+ }
+ // Tab panels.
+ .tab-panel {
+ &--address {
+ .bp3-form-group {
+ max-width: 440px;
+
+ &.bp3-inline {
+ .bp3-label {
+ min-width: 145px;
+ }
+ }
+
+ .bp3-form-content {
+ width: 100%;
+ }
+
+ textarea.bp3-input {
+ max-width: 100%;
+ width: 100%;
+ min-height: 50px;
+ }
+ }
+ }
+ &--note {
+ .form-group--note {
+ .bp3-form-group {
+ max-width: 600px;
+ }
+ textarea {
+ width: 100%;
+ min-height: 100px;
+ }
+ }
+ }
+ }
+
+ .dropzone-container {
+ max-width: 600px;
+ }
+ }
+
+ .bp3-tabs {
+ .bp3-tab-list {
+ position: relative;
+
+ &:before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 2px;
+ background: #f0f0f0;
+ }
+
+ > *:not(:last-child) {
+ margin-right: 25px;
+ }
+
+ &.bp3-large > .bp3-tab {
+ font-size: 15px;
+ color: #555;
+
+ &[aria-selected='true'],
+ &:not([aria-disabled='true']):hover {
+ color: $pt-link-color;
+ }
+ }
+ }
+ }
+}
diff --git a/src/style/pages/Items/PageForm.scss b/src/style/pages/Items/Form.scss
similarity index 85%
rename from src/style/pages/Items/PageForm.scss
rename to src/style/pages/Items/Form.scss
index 9cc4db1d1..31575856d 100644
--- a/src/style/pages/Items/PageForm.scss
+++ b/src/style/pages/Items/Form.scss
@@ -1,16 +1,4 @@
-body.page-item-new,
-body.page-item-edit{
-
- .dashboard__footer{
- display: none;
- }
-}
-
-.dashboard__insider--item-form{
- padding-bottom: 64px;
-}
-
.page-form--item {
$self: '.page-form';
padding: 20px;
@@ -85,18 +73,14 @@ body.page-item-edit{
}
#{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
+ // margin-left: -40px;
+ // margin-right: -40px;
.form-group--active {
display: inline-block;
margin: 0;
margin-left: 40px;
}
-
- .btn--submit{
- min-width: 65px;
- }
}
.bp3-tooltip-indicator {
diff --git a/src/style/pages/SaleInvoice/PageForm.scss b/src/style/pages/SaleInvoice/PageForm.scss
index 2951ad0d4..bfb6be69c 100644
--- a/src/style/pages/SaleInvoice/PageForm.scss
+++ b/src/style/pages/SaleInvoice/PageForm.scss
@@ -1,13 +1,11 @@
-
body.page-invoice-new,
-body.page-invoice-edit{
-
- .dashboard__footer{
+body.page-invoice-edit {
+ .dashboard__footer {
display: none;
}
}
-.dashboard__insider--invoice-form{
+.dashboard__insider--invoice-form {
padding-bottom: 64px;
}
@@ -29,7 +27,6 @@ body.page-invoice-edit{
}
.bp3-form-group {
-
&.bp3-inline {
max-width: 450px;
}
diff --git a/src/style/pages/Customers/PageForm.scss b/src/style/pages/Vendors/Form.scss
similarity index 91%
rename from src/style/pages/Customers/PageForm.scss
rename to src/style/pages/Vendors/Form.scss
index 57512123f..6a341d76c 100644
--- a/src/style/pages/Customers/PageForm.scss
+++ b/src/style/pages/Vendors/Form.scss
@@ -1,19 +1,6 @@
@import '../../Base.scss';
-
-body.page-customer-new,
-body.page-customer-edit{
-
- .dashboard__footer{
- display: none;
- }
-}
-
-.dashboard__insider--customer-form{
- padding-bottom: 64px;
-}
-
-.page-form--customer {
+.page-form--vendor {
$self: '.page-form';
padding: 20px;
@@ -169,7 +156,7 @@ body.page-customer-edit{
}
#{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
+ // margin-left: -40px;
+ // margin-right: -40px;
}
}
diff --git a/src/style/pages/Vendors/PageForm.scss b/src/style/pages/Vendors/PageForm.scss
index dd7e4a372..251f8b3da 100644
--- a/src/style/pages/Vendors/PageForm.scss
+++ b/src/style/pages/Vendors/PageForm.scss
@@ -10,166 +10,4 @@ body.page-vendor-edit{
.dashboard__insider--vendor-form{
padding-bottom: 64px;
-}
-
-
-.page-form--vendor {
- $self: '.page-form';
- padding: 20px;
-
- #{$self}__header {
- padding: 0;
- }
- #{$self}__primary-section {
- padding: 10px 0 0;
- margin: 0 0 20px;
- overflow: hidden;
- border-bottom: 1px solid #e4e4e4;
- max-width: 1000px;
- }
-
- .bp3-form-group {
- max-width: 500px;
-
- .bp3-control {
- margin-top: 8px;
- margin-bottom: 8px;
- }
-
- &.bp3-inline {
- .bp3-label {
- min-width: 150px;
- }
- }
- .bp3-form-content {
- width: 100%;
- }
- }
-
- .form-group--contact_name {
- max-width: 600px;
-
- .bp3-control-group > * {
- flex-shrink: unset;
-
- &:not(:last-child) {
- padding-right: 10px;
- }
- &.input-group--salutation-list {
- width: 25%;
- }
- &.input-group--first-name,
- &.input-group--last-name {
- width: 37%;
- }
- }
- }
-
- .bp3-form-group {
- margin-bottom: 14px;
- }
-
- .bp3-tab-panel {
- margin-top: 26px;
- }
-
- .form-group--phone-number {
- .bp3-control-group > * {
- flex-shrink: unset;
- padding-right: 5px;
- padding-left: 5px;
-
- &:first-child {
- padding-left: 0;
- }
- &:last-child {
- padding-right: 0;
- }
- }
- }
-
- #{$self}__tabs {
- margin-top: 20px;
- max-width: 1000px;
-
- h4 {
- font-weight: 500;
- color: #888;
- margin-bottom: 1.2rem;
- font-size: 14px;
- }
- // Tab panels.
- .tab-panel {
- &--address {
- .bp3-form-group {
- max-width: 440px;
-
- &.bp3-inline {
- .bp3-label {
- min-width: 145px;
- }
- }
-
- .bp3-form-content {
- width: 100%;
- }
-
- textarea.bp3-input {
- max-width: 100%;
- width: 100%;
- min-height: 50px;
- }
- }
- }
- &--note {
- .form-group--note {
- .bp3-form-group {
- max-width: 600px;
- }
- textarea {
- width: 100%;
- min-height: 100px;
- }
- }
- }
- }
-
- .dropzone-container {
- max-width: 600px;
- }
- }
-
- .bp3-tabs {
- .bp3-tab-list {
- position: relative;
-
- &:before {
- content: '';
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 2px;
- background: #f0f0f0;
- }
-
- > *:not(:last-child) {
- margin-right: 25px;
- }
-
- &.bp3-large > .bp3-tab {
- font-size: 15px;
- color: #555;
-
- &[aria-selected='true'],
- &:not([aria-disabled='true']):hover {
- color: $pt-link-color;
- }
- }
- }
- }
-
- #{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
- }
-}
+}
\ No newline at end of file