fix: merge conflict quick create list field.

This commit is contained in:
a.bouhuolia
2021-11-11 00:05:57 +02:00
61 changed files with 1885 additions and 745 deletions

View File

@@ -10,4 +10,8 @@ export const DRAWERS = {
BILL_DRAWER: 'bill-drawer', BILL_DRAWER: 'bill-drawer',
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer', INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer',
CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-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',
}; };

View File

@@ -1,13 +1,55 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core'; import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select'; import { Select } from '@blueprintjs/select';
import { MenuItemNestedText, FormattedMessage as T } from 'components'; import * as R from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils'; import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils'; import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes'; 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 (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
/>
);
};
// 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, accounts,
initialAccountId, initialAccountId,
selectedAccountId, selectedAccountId,
@@ -21,6 +63,8 @@ export default function AccountsSelectList({
filterByNormal, filterByNormal,
filterByRootTypes, filterByRootTypes,
allowCreate,
buttonProps = {}, buttonProps = {},
}) { }) {
const flattenAccounts = useMemo( const flattenAccounts = useMemo(
@@ -51,6 +95,7 @@ export default function AccountsSelectList({
[initialAccountId, filteredAccounts], [initialAccountId, filteredAccounts],
); );
// Select account item.
const [selectedAccount, setSelectedAccount] = useState( const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null, initialAccount || null,
); );
@@ -76,31 +121,25 @@ export default function AccountsSelectList({
); );
}, []); }, []);
const onAccountSelect = useCallback( // Handle the account item select.
const handleAccountSelect = useCallback(
(account) => { (account) => {
setSelectedAccount({ ...account }); if (account.id) {
onAccountSelected && onAccountSelected(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;
} else { } else {
return ( openDialog('account-form');
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
} }
}, },
[], [setSelectedAccount, onAccountSelected, openDialog],
); );
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return ( return (
<Select <Select
items={filteredAccounts} items={filteredAccounts}
@@ -113,11 +152,13 @@ export default function AccountsSelectList({
inline: popoverFill, inline: popoverFill,
}} }}
filterable={true} filterable={true}
onItemSelect={onAccountSelect} onItemSelect={handleAccountSelect}
disabled={disabled} disabled={disabled}
className={classNames('form-group--select-list', { className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
> >
<Button <Button
disabled={disabled} disabled={disabled}
@@ -127,3 +168,5 @@ export default function AccountsSelectList({
</Select> </Select>
); );
} }
export default R.compose(withDialogActions)(AccountsSelectList);

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select'; import { Suggest } from '@blueprintjs/select';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
@@ -10,10 +11,55 @@ import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils'; import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils'; import { nestedArrayToflatten } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
// 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 suggest field. * Accounts suggest field.
*/ */
export default function AccountsSuggestField({ function AccountsSuggestField({
// #withDialogActions
openDialog,
// #ownProps
accounts, accounts,
initialAccountId, initialAccountId,
selectedAccountId, selectedAccountId,
@@ -26,6 +72,8 @@ export default function AccountsSuggestField({
filterByNormal, filterByNormal,
filterByRootTypes = [], filterByRootTypes = [],
allowCreate,
...suggestProps ...suggestProps
}) { }) {
const flattenAccounts = useMemo( const flattenAccounts = useMemo(
@@ -69,23 +117,6 @@ export default function AccountsSuggestField({
} }
}, [selectedAccountId, filteredAccounts, setSelectedAccount]); }, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(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
);
}
},
[],
);
// Account item of select accounts field. // Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => { const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return ( return (
@@ -98,28 +129,31 @@ export default function AccountsSuggestField({
); );
}, []); }, []);
const handleInputValueRenderer = (inputValue) => { const handleAccountSelect = useCallback(
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
const onAccountSelect = useCallback(
(account) => { (account) => {
setSelectedAccount({ ...account }); if (account.id) {
onAccountSelected && onAccountSelected(account); setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog('account-form');
}
}, },
[setSelectedAccount, onAccountSelected], [setSelectedAccount, onAccountSelected, openDialog],
); );
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return ( return (
<Suggest <Suggest
items={filteredAccounts} items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />} noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem} itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater} itemPredicate={filterAccountsPredicater}
onItemSelect={onAccountSelect} onItemSelect={handleAccountSelect}
selectedItem={selectedAccount} selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }} inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true} resetOnClose={true}
@@ -129,7 +163,11 @@ export default function AccountsSuggestField({
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, { className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
{...suggestProps} {...suggestProps}
/> />
); );
} }
export default R.compose(withDialogActions)(AccountsSuggestField);

View File

@@ -11,12 +11,14 @@ export default function ContactSelecetList({
contactsList, contactsList,
initialContactId, initialContactId,
selectedContactId, selectedContactId,
selectedContactType, createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />, defaultSelectText = <T id={'select_contact'} />,
onContactSelected, onContactSelected,
popoverFill = false, popoverFill = false,
disabled = false, disabled = false,
buttonProps, buttonProps,
...restProps
}) { }) {
const contacts = useMemo( const contacts = useMemo(
() => () =>
@@ -65,7 +67,7 @@ export default function ContactSelecetList({
); );
// Filter Contact List // Filter Contact List
const filterContacts = (query, contact, index, exactMatch) => { const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase(); const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase(); const normalizedQuery = query.toLowerCase();
if (exactMatch) { if (exactMatch) {
@@ -83,7 +85,7 @@ export default function ContactSelecetList({
items={contacts} items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />} noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer} itemRenderer={handleContactRenderer}
itemPredicate={filterContacts} itemPredicate={itemPredicate}
filterable={true} filterable={true}
disabled={disabled} disabled={disabled}
onItemSelect={onContactSelect} onItemSelect={onContactSelect}
@@ -92,8 +94,9 @@ export default function ContactSelecetList({
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
inputProps={{ inputProps={{
placeholder: intl.get('filter_') placeholder: intl.get('filter_'),
}} }}
{...restProps}
> >
<Button <Button
disabled={disabled} disabled={disabled}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export default function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
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) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
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_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -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 = <T id={'select_contact'} />,
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 (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
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}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(CustomerSelectField);

View File

@@ -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 = <T id={'select_contact'} />,
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 (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
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}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(VendorSelectField);

View File

@@ -0,0 +1,5 @@
import ContactSelectField from './ContactSelectField';
import CustomerSelectField from './CustomerSelectField';
import VendorSelectField from './VendorSelectField';
export { ContactSelectField, CustomerSelectField, VendorSelectField };

View File

@@ -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 }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={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 (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar'; import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute'; import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary'; import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => { export default React.forwardRef(({}, ref) => {
@@ -11,7 +10,6 @@ export default React.forwardRef(({}, ref) => {
<div className="dashboard-content" id="dashboard" ref={ref}> <div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar /> <DashboardTopbar />
<DashboardContentRoutes /> <DashboardContentRoutes />
<DashboardFooter />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -17,6 +17,8 @@ export default function AccountCellRenderer({
accountsDataProp, accountsDataProp,
filterAccountsByRootTypes, filterAccountsByRootTypes,
filterAccountsByTypes, filterAccountsByTypes,
fieldProps,
formGroupProps,
}, },
row: { index, original }, row: { index, original },
cell: { value: initialValue }, cell: { value: initialValue },
@@ -53,6 +55,7 @@ export default function AccountCellRenderer({
'form-group--account', 'form-group--account',
Classes.FILL, Classes.FILL,
)} )}
{...formGroupProps}
> >
<AccountsSuggestField <AccountsSuggestField
accounts={accounts} accounts={accounts}
@@ -66,6 +69,7 @@ export default function AccountCellRenderer({
}} }}
openOnKeyDown={true} openOnKeyDown={true}
blurOnSelectClose={false} blurOnSelectClose={false}
{...fieldProps}
/> />
</FormGroup> </FormGroup>
); );

View File

@@ -1,15 +1,17 @@
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useRef } from 'react';
// import ItemsListField from 'components/ItemsListField';
import ItemsSuggestField from 'components/ItemsSuggestField';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core'; import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import ItemsSuggestField from 'components/ItemsSuggestField';
import { useCellAutoFocus } from 'hooks'; import { useCellAutoFocus } from 'hooks';
/**
* Items list cell.
*/
export default function ItemsListCell({ export default function ItemsListCell({
column: { id, filterSellable, filterPurchasable }, column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
row: { index }, row: { index },
cell: { value: initialValue }, cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus }, payload: { items, updateData, errors, autoFocus },
@@ -19,6 +21,7 @@ export default function ItemsListCell({
// Auto-focus the items list input field. // Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index); useCellAutoFocus(fieldRef, autoFocus, id, index);
// Handle the item selected.
const handleItemSelected = useCallback( const handleItemSelected = useCallback(
(item) => { (item) => {
updateData(index, id, item.id); updateData(index, id, item.id);
@@ -32,6 +35,7 @@ export default function ItemsListCell({
<FormGroup <FormGroup
intent={error ? Intent.DANGER : null} intent={error ? Intent.DANGER : null}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', Classes.FILL)}
{...formGroupProps}
> >
<ItemsSuggestField <ItemsSuggestField
items={items} items={items}
@@ -45,6 +49,7 @@ export default function ItemsListCell({
}} }}
openOnKeyDown={true} openOnKeyDown={true}
blurOnSelectClose={false} blurOnSelectClose={false}
{...fieldProps}
/> />
</FormGroup> </FormGroup>
); );

View File

@@ -3,6 +3,7 @@ import { Position, Drawer } from '@blueprintjs/core';
import 'style/components/Drawer.scss'; import 'style/components/Drawer.scss';
import { DrawerProvider } from './DrawerProvider';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -27,7 +28,7 @@ function DrawerComponent(props) {
portalClassName={'drawer-portal'} portalClassName={'drawer-portal'}
{...props} {...props}
> >
{children} <DrawerProvider {...props}>{children}</DrawerProvider>
</Drawer> </Drawer>
); );
} }

View File

@@ -0,0 +1,16 @@
import React, { createContext, useContext } from 'react';
const DrawerContext = createContext();
/**
* Account form provider.
*/
function DrawerProvider({ ...props }) {
const provider = { ...props };
return <DrawerContext.Provider value={provider} {...props} />;
}
const useDrawerContext = () => useContext(DrawerContext);
export { DrawerProvider, useDrawerContext };

View File

@@ -14,6 +14,9 @@ import CustomerDetailsDrawer from '../containers/Drawers/CustomerDetailsDrawer';
import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer'; import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer';
import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer'; import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer';
import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTransactionDetailDrawer'; 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'; import { DRAWERS } from 'common/drawers';
@@ -38,7 +41,12 @@ export default function DrawersContainer() {
<InventoryAdjustmentDetailDrawer <InventoryAdjustmentDetailDrawer
name={DRAWERS.INVENTORY_ADJUSTMENT_DRAWER} name={DRAWERS.INVENTORY_ADJUSTMENT_DRAWER}
/> />
<CashflowTransactionDetailDrawer name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER} /> <CashflowTransactionDetailDrawer
name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER}
/>
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
</div> </div>
); );
} }

View File

@@ -1,13 +1,68 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react'; import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { Suggest } from '@blueprintjs/select'; 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'; 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 (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};
// Item renderer.
const itemRenderer = (item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={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, items,
initialItemId, initialItemId,
selectedItemId, selectedItemId,
@@ -18,6 +73,10 @@ export default function ItemsSuggestField({
sellable = false, sellable = false,
purchasable = false, purchasable = false,
popoverFill = false, popoverFill = false,
allowCreate = true,
openDrawer,
...suggestProps ...suggestProps
}) { }) {
// Filters items based on filter props. // Filters items based on filter props.
@@ -36,28 +95,23 @@ export default function ItemsSuggestField({
// Find initial item object. // Find initial item object.
const initialItem = useMemo( const initialItem = useMemo(
() => filteredItems.some((a) => a.id === initialItemId), () => filteredItems.some((a) => a.id === initialItemId),
[initialItemId], [initialItemId, filteredItems],
); );
const [selectedItem, setSelectedItem] = useState(initialItem || null); const [selectedItem, setSelectedItem] = useState(initialItem || null);
const onItemSelect = useCallback( const onItemSelect = useCallback(
(item) => { (item) => {
setSelectedItem({ ...item }); if (item.id) {
onItemSelected && onItemSelected(item); setSelectedItem({ ...item });
onItemSelected && onItemSelected(item);
} else {
openDrawer(DRAWERS.QUICK_CREATE_ITEM);
}
}, },
[setSelectedItem, onItemSelected], [setSelectedItem, onItemSelected, openDrawer],
); );
const itemRenderer = useCallback((item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
));
useEffect(() => { useEffect(() => {
if (typeof selectedItemId !== 'undefined') { if (typeof selectedItemId !== 'undefined') {
const item = selectedItemId const item = selectedItemId
@@ -67,27 +121,12 @@ export default function ItemsSuggestField({
} }
}, [selectedItemId, filteredItems, setSelectedItem]); }, [selectedItemId, filteredItems, setSelectedItem]);
const handleInputValueRenderer = (inputValue) => { // Maybe inject create new item props to suggest component.
if (inputValue) { const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
return inputValue.name.toString(); const maybeCreateNewItemFromQuery = allowCreate
} ? createNewItemFromQuery
return ''; : 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 ( return (
<Suggest <Suggest
items={filteredItems} items={filteredItems}
@@ -104,7 +143,12 @@ export default function ItemsSuggestField({
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, { className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...suggestProps} {...suggestProps}
/> />
); );
} }
export default R.compose(withDrawerActions)(ItemsSuggestField);

View File

@@ -86,6 +86,7 @@ export * from './Datatable/CellForceWidth';
export * from './Button'; export * from './Button';
export * from './IntersectionObserver'; export * from './IntersectionObserver';
export * from './SMSPreview'; export * from './SMSPreview';
export * from './Contacts';
const Hint = FieldHint; const Hint = FieldHint;

View File

@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
className: 'account', className: 'account',
disableSortBy: true, disableSortBy: true,
width: 160, width: 160,
fieldProps: { allowCreate: true }
}, },
{ {
Header: CreditHeaderCell, Header: CreditHeaderCell,

View File

@@ -9,23 +9,24 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import styled from 'styled-components';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { Icon } from 'components'; import { Icon } from 'components';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from 'utils';
/** /**
* Customer floating actions bar. * Customer floating actions bar.
*/ */
export default function CustomerFloatingActions() { export default function CustomerFloatingActions({ onCancel }) {
const history = useHistory();
// Customer form context. // Customer form context.
const { isNewMode,setSubmitPayload } = useCustomerFormContext(); const { isNewMode, setSubmitPayload } = useCustomerFormContext();
// Formik context. // Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext(); const { resetForm, submitForm, isSubmitting } = useFormikContext();
@@ -37,7 +38,7 @@ export default function CustomerFloatingActions() {
// Handle cancel button click. // Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); safeInvoke(onCancel, event);
}; };
// handle clear button clicl. // handle clear button clicl.
@@ -55,7 +56,7 @@ export default function CustomerFloatingActions() {
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<Button <SaveButton
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
@@ -83,6 +84,7 @@ export default function CustomerFloatingActions() {
/> />
</Popover> </Popover>
</ButtonGroup> </ButtonGroup>
{/* ----------- Clear & Reset----------- */} {/* ----------- Clear & Reset----------- */}
<Button <Button
className={'ml1'} className={'ml1'}
@@ -99,3 +101,7 @@ export default function CustomerFloatingActions() {
</div> </div>
); );
} }
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -1,152 +1,14 @@
import React, { useMemo } from 'react'; import React from 'react';
import { Formik, Form } from 'formik'; import { CustomerFormProvider } from './CustomerFormProvider';
import moment from 'moment'; import CustomerFormFormik from './CustomerFormFormik';
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'),
};
/** /**
* Customer form. * Abstructed customer form.
*/ */
function CustomerForm({ organization: { base_currency } }) { export default function CustomerForm({ customerId }) {
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);
}
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}> <CustomerFormProvider customerId={customerId}>
<Formik <CustomerFormFormik />
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm} </CustomerFormProvider>
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions />
</Form>
</Formik>
</div>
); );
} }
export default compose(withCurrentOrganization())(CustomerForm);

View File

@@ -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 (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_CUSTOMER,
className,
)}
>
<Formik
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}
export default compose(withCurrentOrganization())(CustomerFormFormik);

View File

@@ -1,20 +1,74 @@
import React from 'react'; 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 { DashboardCard } from 'components';
import CustomerForm from './CustomerForm'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { CustomerFormProvider } from './CustomerFormProvider';
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 ( return (
<CustomerFormProvider customerId={id}> <CustomerDashboardInsider loading={isFormLoading}>
<DashboardCard page> {children}
<CustomerForm /> </CustomerDashboardInsider>
</DashboardCard> );
}
/**
* 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 (
<CustomerFormProvider customerId={customerId}>
<CustomerFormPageLoading>
<DashboardCard page>
<CustomerFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</CustomerFormPageLoading>
</CustomerFormProvider> </CustomerFormProvider>
); );
} }
const CustomerFormPageFormik = styled(CustomerFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;

View File

@@ -1,6 +1,5 @@
import React, { useState, createContext } from 'react'; import React, { useState, createContext } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { import {
useCustomer, useCustomer,
useCurrencies, useCurrencies,
@@ -24,7 +23,7 @@ function CustomerFormProvider({ customerId, ...props }) {
// Handle fetch contact duplicate details. // Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact( const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId, contactId,
{ enabled: !!contactId, }, { enabled: !!contactId },
); );
// Handle fetch Currencies data table // Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(); const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
@@ -38,6 +37,9 @@ function CustomerFormProvider({ customerId, ...props }) {
// determines whether the form new or duplicate mode. // determines whether the form new or duplicate mode.
const isNewMode = contactId || !customerId; const isNewMode = contactId || !customerId;
const isFormLoading =
isCustomerLoading || isCurrenciesLoading || isContactLoading;
const provider = { const provider = {
customerId, customerId,
customer, customer,
@@ -48,24 +50,14 @@ function CustomerFormProvider({ customerId, ...props }) {
isCustomerLoading, isCustomerLoading,
isCurrenciesLoading, isCurrenciesLoading,
isFormLoading,
setSubmitPayload, setSubmitPayload,
editCustomerMutate, editCustomerMutate,
createCustomerMutate, createCustomerMutate,
}; };
return ( return <CustomerFormContext.Provider value={provider} {...props} />;
<DashboardInsider
loading={
isCustomerLoading ||
isCurrenciesLoading ||
isContactLoading
}
name={'customer-form'}
>
<CustomerFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
} }
const useCustomerFormContext = () => React.useContext(CustomerFormContext); const useCustomerFormContext = () => React.useContext(CustomerFormContext);

View File

@@ -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'),
};

View File

@@ -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 (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-customer"
title={<T id={'create_a_new_customer'} />}
/>
<DrawerBody>
<QuickCustomerFormDrawer displayName={displayName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -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 <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* 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 (
<CustomerFormProvider customerId={customerId}>
<DrawerCustomerFormLoading>
<CustomerFormCard>
<CustomerFormFormik
initialValues={{ display_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</CustomerFormCard>
</DrawerCustomerFormLoading>
</CustomerFormProvider>
);
}
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;
}
}
`;

View File

@@ -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 (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'80%'}
>
<DrawerSuspense>
<QuickCreateCustomerDrawerContent displayName={payload.displayName} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(QuickCreateCustomerDrawer);

View File

@@ -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 (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-item"
title={<T id={'create_a_new_item'} />}
/>
<DrawerBody>
<QuickCreateItemDrawerForm itemName={itemName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -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 <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* 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 (
<ItemFormProvider itemId={itemId}>
<DrawerItemFormLoading>
<ItemFormCard>
<ItemFormFormik
initialValues={{ name: itemName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</ItemFormCard>
</DrawerItemFormLoading>
</ItemFormProvider>
);
}
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;
}
`;

View File

@@ -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 (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '800px', maxWidth: '1000px' }}
size={'72%'}
payload={payload}
>
<DrawerSuspense>
<QuickCretaeItemDrawerContent itemName={payload.name} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(QuickCreateItemDrawer);

View File

@@ -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 <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* 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 (
<VendorFormProvider vendorId={vendorId}>
<DrawerVendorFormLoading>
<VendorFormCard>
<VendorFormFormik
initialValues={{ display_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</VendorFormCard>
</DrawerVendorFormLoading>
</VendorFormProvider>
);
}
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;
}
}
`;

View File

@@ -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 (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-customer"
title={"Create a new vendor"}
/>
<DrawerBody>
<QuickVendorFormDrawer displayName={displayName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -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 (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'80%'}
payload={payload}
>
<DrawerSuspense>
<QuickWriteVendorDrawerContent displayName={payload.displayName} />
</DrawerSuspense>
</Drawer>
);
}
export default R.compose(withDrawers())(QuickWriteVendorDrawer);

View File

@@ -108,7 +108,9 @@ const LandedCostHeaderCell = () => {
/** /**
* Retrieve editable items entries columns. * Retrieve editable items entries columns.
*/ */
export function useEditableItemsEntriesColumns({ landedCost }) { export function useEditableItemsEntriesColumns({
landedCost,
}) {
return React.useMemo( return React.useMemo(
() => [ () => [
{ {
@@ -129,6 +131,7 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
disableSortBy: true, disableSortBy: true,
width: 130, width: 130,
className: 'item', className: 'item',
fieldProps: { allowCreate: true },
}, },
{ {
Header: intl.get('description'), Header: intl.get('description'),

View File

@@ -164,3 +164,12 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
updateTableRow(rowIndex, newRow), updateTableRow(rowIndex, newRow),
)(rows); )(rows);
}); });
/**
*
* @param {*} entries
* @returns
*/
export const composeControlledEntries = (entries) => {
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
};

View File

@@ -14,7 +14,7 @@ import {
import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils'; import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils';
import { import {
CurrencySelectList, CurrencySelectList,
ContactSelecetList, CustomerSelectField,
AccountsSelectList, AccountsSelectList,
FieldRequiredHint, FieldRequiredHint,
Hint, Hint,
@@ -78,6 +78,7 @@ export default function ExpenseFormHeader() {
defaultSelectText={<T id={'select_payment_account'} />} defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={value} selectedAccountId={value}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -137,13 +138,14 @@ export default function ExpenseFormHeader() {
helperText={<ErrorMessage name={'assign_to_customer'} />} helperText={<ErrorMessage name={'assign_to_customer'} />}
inline={true} inline={true}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => { onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id); form.setFieldValue('customer_id', customer.id);
}} }}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -113,6 +113,7 @@ export function useExpenseFormTableColumns({ landedCost }) {
disableSortBy: true, disableSortBy: true,
width: 40, width: 40,
filterAccountsByRootTypes: ['expense'], filterAccountsByRootTypes: ['expense'],
fieldProps: { allowCreate: true },
}, },
{ {
Header: ExpenseAmountHeaderCell, Header: ExpenseAmountHeaderCell,

View File

@@ -1,110 +1,95 @@
import React from 'react'; 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 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 ItemFormFormik from './ItemFormFormik';
import AppToaster from 'components/AppToaster';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import { import DashboardCard from 'components/Dashboard/DashboardCard';
transformSubmitRequestErrors, import DashboardInsider from 'components/Dashboard/DashboardInsider';
useItemFormInitialValues,
} from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { useItemFormContext } from './ItemFormProvider';
/** /**
* Item form. * Item form dashboard title.
* @returns {null}
*/ */
export default function ItemForm() { function ItemFormDashboardTitle() {
// Item form context. // Change page title dispatcher.
const { const changePageTitle = useDashboardPageTitle();
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode,
} = useItemFormContext();
// 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 (
<DashboardItemFormPageInsider loading={isFormLoading} name={'item-form'}>
{children}
</DashboardItemFormPageInsider>
);
}
/**
* Item form of the page.
* @returns {JSX}
*/
export default function ItemForm({ itemId }) {
// History context. // History context.
const history = useHistory(); const history = useHistory();
// Initial values in create and edit mode. // Handle the form submit success.
const initialValues = useItemFormInitialValues(item); const handleSubmitSuccess = (values, form, submitPayload) => {
if (submitPayload.redirect) {
// Handles the form submit. history.push('/items');
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 cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return ( return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}> <ItemFormProvider itemId={itemId}>
<Formik <ItemFormDashboardTitle />
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions /> <ItemFormPageLoading>
</Form> <DashboardCard page>
</Formik> <ItemFormPageFormik
</div> onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</ItemFormPageLoading>
</ItemFormProvider>
); );
} }
const DashboardItemFormPageInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
const ItemFormPageFormik = styled(ItemFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useFormikContext, FastField, Field, ErrorMessage } from 'formik'; import { useFormikContext, FastField, ErrorMessage } from 'formik';
import { import {
FormGroup, FormGroup,
Classes, Classes,
@@ -122,6 +122,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.sellable} disabled={!form.values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.purchasable} disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -1,20 +1,19 @@
import React from 'react'; import React from 'react';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core'; import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components'; import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { FastField, useFormikContext } from 'formik'; import { FastField, useFormikContext } from 'formik';
import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { saveInvoke } from '../../utils';
/** /**
* Item form floating actions. * Item form floating actions.
*/ */
export default function ItemFormFloatingActions() { export default function ItemFormFloatingActions({ onCancel }) {
// History context.
const history = useHistory();
// Item form context. // Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext(); const { setSubmitPayload, isNewMode } = useItemFormContext();
@@ -23,7 +22,7 @@ export default function ItemFormFloatingActions() {
// Handle cancel button click. // Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); saveInvoke(onCancel, event);
}; };
// Handle submit button click. // Handle submit button click.
@@ -38,7 +37,7 @@ export default function ItemFormFloatingActions() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button <SaveButton
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
@@ -47,7 +46,7 @@ export default function ItemFormFloatingActions() {
className={'btn--submit'} className={'btn--submit'}
> >
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />} {isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
</Button> </SaveButton>
<Button <Button
className={classNames('ml1', 'btn--submit-new')} className={classNames('ml1', 'btn--submit-new')}
@@ -82,3 +81,7 @@ export default function ItemFormFloatingActions() {
</div> </div>
); );
} }
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -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 (
<div class={classNames(CLASSES.PAGE_FORM_ITEM, className)}>
<Formik
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}

View File

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

View File

@@ -1,8 +1,5 @@
import React, { useEffect, createContext, useState } from 'react'; import React, { createContext, useState } from 'react';
import intl from 'react-intl-universal';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { import {
useItem, useItem,
useSettingsItems, useSettingsItems,
@@ -11,7 +8,6 @@ import {
useEditItem, useEditItem,
useAccounts, useAccounts,
} from 'hooks/query'; } from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state';
import { useWatchItemError } from './utils'; import { useWatchItemError } from './utils';
const ItemFormContext = createContext(); const ItemFormContext = createContext();
@@ -59,6 +55,13 @@ function ItemFormProvider({ itemId, ...props }) {
// Detarmines whether the form new mode. // Detarmines whether the form new mode.
const isNewMode = duplicateId || !itemId; const isNewMode = duplicateId || !itemId;
// Detarmines the form loading state.
const isFormLoading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
// Provider state. // Provider state.
const provider = { const provider = {
itemId, itemId,
@@ -68,6 +71,7 @@ function ItemFormProvider({ itemId, ...props }) {
submitPayload, submitPayload,
isNewMode, isNewMode,
isFormLoading,
isAccountsLoading, isAccountsLoading,
isItemsCategoriesLoading, isItemsCategoriesLoading,
isItemLoading, isItemLoading,
@@ -77,27 +81,7 @@ function ItemFormProvider({ itemId, ...props }) {
setSubmitPayload, setSubmitPayload,
}; };
// Change page title dispatcher. return <ItemFormContext.Provider value={provider} {...props} />;
const changePageTitle = useDashboardPageTitle();
// Changes the page title in new and edit mode.
useEffect(() => {
isNewMode
? changePageTitle(intl.get('new_item'))
: changePageTitle(intl.get('edit_item_details'));
}, [changePageTitle, isNewMode]);
const loading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
return (
<DashboardInsider loading={loading} name={'item-form'}>
<ItemFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
} }
const useItemFormContext = () => React.useContext(ItemFormContext); const useItemFormContext = () => React.useContext(ItemFormContext);

View File

@@ -33,7 +33,7 @@ const defaultInitialValues = {
/** /**
* Initial values in create and edit mode. * Initial values in create and edit mode.
*/ */
export const useItemFormInitialValues = (item) => { export const useItemFormInitialValues = (item, initialValues) => {
const { items: itemsSettings } = useSettingsSelector(); const { items: itemsSettings } = useSettingsSelector();
return useMemo( return useMemo(
@@ -54,8 +54,9 @@ export const useItemFormInitialValues = (item) => {
transformItemFormData(item, defaultInitialValues), transformItemFormData(item, defaultInitialValues),
defaultInitialValues, defaultInitialValues,
), ),
...initialValues,
}), }),
[item, itemsSettings], [item, itemsSettings, initialValues],
); );
}; };

View File

@@ -6,7 +6,7 @@ import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { ContactSelecetList, FieldRequiredHint, Icon } from 'components'; import { VendorSelectField, FieldRequiredHint, Icon } from 'components';
import { vendorsFieldShouldUpdate } from './utils'; import { vendorsFieldShouldUpdate } from './utils';
import { useBillFormContext } from './BillFormProvider'; import { useBillFormContext } from './BillFormProvider';
@@ -43,14 +43,15 @@ function BillFormHeader() {
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />} helperText={<ErrorMessage name={'vendor_id'} />}
> >
<ContactSelecetList <VendorSelectField
contactsList={vendors} contacts={vendors}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />} defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => { onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id); form.setFieldValue('vendor_id', contact.id);
}} }}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -15,7 +15,7 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
AccountsSelectList, AccountsSelectList,
ContactSelecetList, VendorSelectField,
FieldRequiredHint, FieldRequiredHint,
InputPrependText, InputPrependText,
Money, Money,
@@ -90,8 +90,8 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />} helperText={<ErrorMessage name={'vendor_id'} />}
> >
<ContactSelecetList <VendorSelectField
contactsList={vendors} contacts={vendors}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />} defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => { onContactSelected={(contact) => {
@@ -100,6 +100,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
}} }}
disabled={!isNewMode} disabled={!isNewMode}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -19,7 +19,7 @@ import { customersFieldShouldUpdate } from './utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
ContactSelecetList, CustomerSelectField,
FieldRequiredHint, FieldRequiredHint,
Icon, Icon,
InputPrependButton, InputPrependButton,
@@ -82,8 +82,8 @@ function EstimateFormHeader({
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />} helperText={<ErrorMessage name={'customer_id'} />}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => { onContactSelected={(customer) => {
@@ -91,6 +91,7 @@ function EstimateFormHeader({
}} }}
popoverFill={true} popoverFill={true}
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -10,20 +10,23 @@ import { FastField, Field, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
import { momentFormatter, compose, tansformDateValue } from 'utils'; import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
useObserveInvoiceNoSettings, useObserveInvoiceNoSettings,
customerNameFieldShouldUpdate, customerNameFieldShouldUpdate,
} from './utils'; } from './utils';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
ContactSelecetList, CustomerSelectField,
FieldRequiredHint, FieldRequiredHint,
Icon, Icon,
InputPrependButton, InputPrependButton,
} from 'components'; } from 'components';
import { useInvoiceFormContext } from './InvoiceFormProvider'; import { useInvoiceFormContext } from './InvoiceFormProvider';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils'; import { inputIntent, handleDateChange } from 'utils';
/** /**
@@ -84,14 +87,15 @@ function InvoiceFormHeaderFields({
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />} helperText={<ErrorMessage name={'customer_id'} />}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => { onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id); form.setFieldValue('customer_id', customer.id);
}} }}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -23,7 +23,7 @@ import {
} from 'utils'; } from 'utils';
import { import {
AccountsSelectList, AccountsSelectList,
ContactSelecetList, CustomerSelectField,
FieldRequiredHint, FieldRequiredHint,
Icon, Icon,
InputPrependButton, InputPrependButton,
@@ -134,8 +134,8 @@ function PaymentReceiveHeaderFields({
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />} helperText={<ErrorMessage name={'customer_id'} />}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => { onContactSelected={(customer) => {
@@ -147,6 +147,7 @@ function PaymentReceiveHeaderFields({
buttonProps={{ buttonProps={{
elementRef: (ref) => (customerFieldRef.current = ref), elementRef: (ref) => (customerFieldRef.current = ref),
}} }}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -12,7 +12,7 @@ import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
AccountsSelectList, AccountsSelectList,
ContactSelecetList, CustomerSelectField,
FieldRequiredHint, FieldRequiredHint,
Icon, Icon,
InputPrependButton, InputPrependButton,
@@ -88,14 +88,15 @@ function ReceiptFormHeader({
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />} helperText={<ErrorMessage name={'customer_id'} />}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => { onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id); form.setFieldValue('customer_id', contact.id);
}} }}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -129,6 +130,7 @@ function ReceiptFormHeader({
ACCOUNT_TYPE.BANK, ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET, ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]} ]}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -9,42 +9,43 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components'; import styled from 'styled-components';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useFormikContext } from 'formik'; 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 { Icon } from 'components';
import { useVendorFormContext } from './VendorFormProvider'; import { useVendorFormContext } from './VendorFormProvider';
import { safeInvoke } from 'utils';
/** /**
* Vendor floating actions bar. * Vendor floating actions bar.
*/ */
export default function VendorFloatingActions() { export default function VendorFloatingActions({ onCancel }) {
// Formik context. // Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext(); const { resetForm, isSubmitting, submitForm } = useFormikContext();
// Vendor form context. // Vendor form context.
const { isNewMode, setSubmitPayload } = useVendorFormContext(); const { isNewMode, setSubmitPayload } = useVendorFormContext();
// History.
const history = useHistory();
// Handle the submit button. // Handle the submit button.
const handleSubmitBtnClick = (event) => { const handleSubmitBtnClick = (event) => {
setSubmitPayload({ noRedirect: false, }); setSubmitPayload({ noRedirect: false });
submitForm(); submitForm();
}; };
// Handle the submit & new button click. // Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => { const handleSubmitAndNewClick = (event) => {
submitForm(); submitForm();
setSubmitPayload({ noRedirect: true, }); setSubmitPayload({ noRedirect: true });
}; };
// Handle cancel button click. // Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); safeInvoke(onCancel, event);
}; };
// Handle clear button click. // Handle clear button click.
@@ -56,7 +57,7 @@ export default function VendorFloatingActions() {
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<Button <SaveButton
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
@@ -101,3 +102,7 @@ export default function VendorFloatingActions() {
</div> </div>
); );
} }
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -1,13 +1,10 @@
import React, { useMemo, useEffect } from 'react'; import React, { useMemo } from 'react';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'components';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import { import {
CreateVendorFormSchema, CreateVendorFormSchema,
@@ -19,56 +16,27 @@ import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection';
import VendorTabs from './VendorsTabs'; import VendorTabs from './VendorsTabs';
import VendorFloatingActions from './VendorFloatingActions'; import VendorFloatingActions from './VendorFloatingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization'; import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { useVendorFormContext } from './VendorFormProvider'; import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm } from 'utils'; import { compose, transformToForm, safeInvoke } from 'utils';
const defaultInitialValues = { import { defaultInitialValues } from './utils';
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '', import 'style/pages/Vendors/Form.scss';
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'),
};
/** /**
* Vendor form. * Vendor form.
*/ */
function VendorForm({ function VendorFormFormik({
// #withDashboardActions
changePageTitle,
// #withCurrentOrganization // #withCurrentOrganization
organization: { base_currency }, organization: { base_currency },
// #ownProps
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) { }) {
// Vendor form context. // Vendor form context.
const { const {
@@ -82,11 +50,6 @@ function VendorForm({
isNewMode, isNewMode,
} = useVendorFormContext(); } = useVendorFormContext();
// const isNewMode = !vendorId;
// History context.
const history = useHistory();
/** /**
* Initial values in create and edit mode. * Initial values in create and edit mode.
*/ */
@@ -101,14 +64,13 @@ function VendorForm({
); );
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = ( const handleFormSubmit = (values, form) => {
values, const { setSubmitting, resetForm } = form;
{ setSubmitting, resetForm, setErrors },
) => {
const requestForm = { ...values }; const requestForm = { ...values };
setSubmitting(true); setSubmitting(true);
const onSuccess = () => { const onSuccess = (response) => {
AppToaster.show({ AppToaster.show({
message: intl.get( message: intl.get(
isNewMode isNewMode
@@ -121,16 +83,15 @@ function VendorForm({
setSubmitting(false); setSubmitting(false);
resetForm(); resetForm();
if (!submitPayload.noRedirect) { safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
history.push('/vendors');
}
}; };
const onError = () => { const onError = () => {
setSubmitPayload(false); setSubmitPayload(false);
setSubmitting(false); setSubmitting(false);
};
safeInvoke(onSubmitError, values, form, submitPayload);
};
if (isNewMode) { if (isNewMode) {
createVendorMutate(requestForm).then(onSuccess).catch(onError); createVendorMutate(requestForm).then(onSuccess).catch(onError);
} else { } else {
@@ -139,7 +100,13 @@ function VendorForm({
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_VENDOR)}> <div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_VENDOR,
className,
)}
>
<Formik <Formik
validationSchema={ validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
@@ -160,14 +127,11 @@ function VendorForm({
<VendorTabs vendor={vendorId} /> <VendorTabs vendor={vendorId} />
</div> </div>
<VendorFloatingActions /> <VendorFloatingActions onCancel={onCancel} />
</Form> </Form>
</Formik> </Formik>
</div> </div>
); );
} }
export default compose( export default compose(withCurrentOrganization())(VendorFormFormik);
withDashboardActions,
withCurrentOrganization(),
)(VendorForm);

View File

@@ -1,26 +1,69 @@
import React from 'react'; 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 'style/pages/Vendors/PageForm.scss';
import { DashboardCard } from 'components'; import { DashboardCard, DashboardInsider } from 'components';
import VendorFrom from './VendorForm'; 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 (
<VendorDashboardInsider loading={isFormLoading}>
{children}
</VendorDashboardInsider>
);
}
/** /**
* Vendor form page. * Vendor form page.
*/ */
function VendorFormPage() { export default function VendorFormPage() {
const history = useHistory();
const { id } = useParams(); 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 ( return (
<VendorFormProvider vendorId={id}> <VendorFormProvider vendorId={id}>
<DashboardCard page> <VendorFormPageLoading>
<VendorFrom /> <DashboardCard page>
</DashboardCard> <VendorFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</VendorFormPageLoading>
</VendorFormProvider> </VendorFormProvider>
); );
} }
export default VendorFormPage; const VendorFormPageFormik = styled(VendorFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const VendorDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;

View File

@@ -6,7 +6,6 @@ import {
useVendor, useVendor,
useContact, useContact,
useCurrencies, useCurrencies,
useCustomer,
useCreateVendor, useCreateVendor,
useEditVendor, useEditVendor,
} from 'hooks/query'; } from 'hooks/query';
@@ -30,11 +29,10 @@ function VendorFormProvider({ vendorId, ...props }) {
}); });
// Handle fetch contact duplicate details. // Handle fetch contact duplicate details.
const { const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
data: contactDuplicate, contactId,
isLoading: isContactLoading, { enabled: !!contactId },
} = useContact(contactId, { enabled: !!contactId }); );
// Create and edit vendor mutations. // Create and edit vendor mutations.
const { mutateAsync: createVendorMutate } = useCreateVendor(); const { mutateAsync: createVendorMutate } = useCreateVendor();
const { mutateAsync: editVendorMutate } = useEditVendor(); const { mutateAsync: editVendorMutate } = useEditVendor();
@@ -45,27 +43,25 @@ function VendorFormProvider({ vendorId, ...props }) {
// determines whether the form new or duplicate mode. // determines whether the form new or duplicate mode.
const isNewMode = contactId || !vendorId; const isNewMode = contactId || !vendorId;
const isFormLoading =
isVendorLoading || isContactLoading || isCurrenciesLoading;
const provider = { const provider = {
vendorId, vendorId,
currencies, currencies,
vendor, vendor,
contactDuplicate: { ...omit(contactDuplicate, ['opening_balance_at']) }, contactDuplicate: { ...omit(contactDuplicate, ['opening_balance_at']) },
submitPayload, submitPayload,
isNewMode, isNewMode,
isFormLoading,
createVendorMutate, createVendorMutate,
editVendorMutate, editVendorMutate,
setSubmitPayload, setSubmitPayload,
}; };
return ( return <VendorFormContext.Provider value={provider} {...props} />;
<DashboardInsider
loading={isVendorLoading || isContactLoading || isCurrenciesLoading}
name={'vendor-form'}
>
<VendorFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
} }
const useVendorFormContext = () => React.useContext(VendorFormContext); const useVendorFormContext = () => React.useContext(VendorFormContext);

View File

@@ -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'),
};

View File

@@ -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;
}
}
}
}
}

View File

@@ -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 { .page-form--item {
$self: '.page-form'; $self: '.page-form';
padding: 20px; padding: 20px;
@@ -85,18 +73,14 @@ body.page-item-edit{
} }
#{$self}__floating-actions { #{$self}__floating-actions {
margin-left: -40px; // margin-left: -40px;
margin-right: -40px; // margin-right: -40px;
.form-group--active { .form-group--active {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
margin-left: 40px; margin-left: 40px;
} }
.btn--submit{
min-width: 65px;
}
} }
.bp3-tooltip-indicator { .bp3-tooltip-indicator {

View File

@@ -1,13 +1,11 @@
body.page-invoice-new, body.page-invoice-new,
body.page-invoice-edit{ body.page-invoice-edit {
.dashboard__footer {
.dashboard__footer{
display: none; display: none;
} }
} }
.dashboard__insider--invoice-form{ .dashboard__insider--invoice-form {
padding-bottom: 64px; padding-bottom: 64px;
} }
@@ -29,7 +27,6 @@ body.page-invoice-edit{
} }
.bp3-form-group { .bp3-form-group {
&.bp3-inline { &.bp3-inline {
max-width: 450px; max-width: 450px;
} }

View File

@@ -1,19 +1,6 @@
@import '../../Base.scss'; @import '../../Base.scss';
.page-form--vendor {
body.page-customer-new,
body.page-customer-edit{
.dashboard__footer{
display: none;
}
}
.dashboard__insider--customer-form{
padding-bottom: 64px;
}
.page-form--customer {
$self: '.page-form'; $self: '.page-form';
padding: 20px; padding: 20px;
@@ -169,7 +156,7 @@ body.page-customer-edit{
} }
#{$self}__floating-actions { #{$self}__floating-actions {
margin-left: -40px; // margin-left: -40px;
margin-right: -40px; // margin-right: -40px;
} }
} }

View File

@@ -11,165 +11,3 @@ body.page-vendor-edit{
.dashboard__insider--vendor-form{ .dashboard__insider--vendor-form{
padding-bottom: 64px; 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;
}
}