mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: quick create action on select/suggest items fields.
This commit is contained in:
@@ -1,110 +1,95 @@
|
||||
import React from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import 'style/pages/Items/PageForm.scss';
|
||||
import { useDashboardPageTitle } from 'hooks/state';
|
||||
import { useItemFormContext, ItemFormProvider } from './ItemFormProvider';
|
||||
|
||||
import { CLASSES } from 'common/classes';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import ItemFormPrimarySection from './ItemFormPrimarySection';
|
||||
import ItemFormBody from './ItemFormBody';
|
||||
import ItemFormFloatingActions from './ItemFormFloatingActions';
|
||||
import ItemFormInventorySection from './ItemFormInventorySection';
|
||||
import ItemFormFormik from './ItemFormFormik';
|
||||
|
||||
import {
|
||||
transformSubmitRequestErrors,
|
||||
useItemFormInitialValues,
|
||||
} from './utils';
|
||||
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
|
||||
|
||||
import { useItemFormContext } from './ItemFormProvider';
|
||||
import DashboardCard from 'components/Dashboard/DashboardCard';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
/**
|
||||
* Item form.
|
||||
* Item form dashboard title.
|
||||
* @returns {null}
|
||||
*/
|
||||
export default function ItemForm() {
|
||||
// Item form context.
|
||||
const {
|
||||
itemId,
|
||||
item,
|
||||
accounts,
|
||||
createItemMutate,
|
||||
editItemMutate,
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
} = useItemFormContext();
|
||||
function ItemFormDashboardTitle() {
|
||||
// Change page title dispatcher.
|
||||
const changePageTitle = useDashboardPageTitle();
|
||||
|
||||
// Item form context.
|
||||
const { isNewMode } = useItemFormContext();
|
||||
|
||||
// Changes the page title in new and edit mode.
|
||||
React.useEffect(() => {
|
||||
isNewMode
|
||||
? changePageTitle(intl.get('new_item'))
|
||||
: changePageTitle(intl.get('edit_item_details'));
|
||||
}, [changePageTitle, isNewMode]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item form page loading state indicator.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
function ItemFormPageLoading({ children }) {
|
||||
const { isFormLoading } = useItemFormContext();
|
||||
|
||||
return (
|
||||
<DashboardItemFormPageInsider loading={isFormLoading} name={'item-form'}>
|
||||
{children}
|
||||
</DashboardItemFormPageInsider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item form of the page.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
export default function ItemForm({ itemId }) {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useItemFormInitialValues(item);
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (
|
||||
values,
|
||||
{ setSubmitting, resetForm, setErrors },
|
||||
) => {
|
||||
setSubmitting(true);
|
||||
const form = { ...values };
|
||||
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get(
|
||||
isNewMode
|
||||
? 'the_item_has_been_created_successfully'
|
||||
: 'the_item_has_been_edited_successfully',
|
||||
{
|
||||
number: itemId,
|
||||
},
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
|
||||
// Submit payload.
|
||||
if (submitPayload.redirect) {
|
||||
history.push('/items');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle response error.
|
||||
const onError = (errors) => {
|
||||
setSubmitting(false);
|
||||
if (errors) {
|
||||
const _errors = transformSubmitRequestErrors(errors);
|
||||
setErrors({ ..._errors });
|
||||
}
|
||||
};
|
||||
if (isNewMode) {
|
||||
createItemMutate(form).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
editItemMutate([itemId, form]).then(onSuccess).catch(onError);
|
||||
// Handle the form submit success.
|
||||
const handleSubmitSuccess = (values, form, submitPayload) => {
|
||||
if (submitPayload.redirect) {
|
||||
history.push('/items');
|
||||
}
|
||||
};
|
||||
// Handle cancel button click.
|
||||
const handleFormCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}>
|
||||
<Formik
|
||||
enableReinitialize={true}
|
||||
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Form>
|
||||
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<ItemFormPrimarySection />
|
||||
<ItemFormBody accounts={accounts} />
|
||||
<ItemFormInventorySection accounts={accounts} />
|
||||
</div>
|
||||
<ItemFormProvider itemId={itemId}>
|
||||
<ItemFormDashboardTitle />
|
||||
|
||||
<ItemFormFloatingActions />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
<ItemFormPageLoading>
|
||||
<DashboardCard page>
|
||||
<ItemFormPageFormik
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useFormikContext, FastField, Field, ErrorMessage } from 'formik';
|
||||
import { useFormikContext, FastField, ErrorMessage } from 'formik';
|
||||
import {
|
||||
FormGroup,
|
||||
Classes,
|
||||
@@ -122,6 +122,7 @@ function ItemFormBody({ organization: { base_currency } }) {
|
||||
disabled={!form.values.sellable}
|
||||
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
|
||||
popoverFill={true}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
|
||||
disabled={!form.values.purchasable}
|
||||
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
|
||||
popoverFill={true}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { FastField, useFormikContext } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FormattedMessage as T } from 'components';
|
||||
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { useItemFormContext } from './ItemFormProvider';
|
||||
import { saveInvoke } from '../../utils';
|
||||
|
||||
/**
|
||||
* Item form floating actions.
|
||||
*/
|
||||
export default function ItemFormFloatingActions() {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
|
||||
export default function ItemFormFloatingActions({ onCancel }) {
|
||||
// Item form context.
|
||||
const { setSubmitPayload, isNewMode } = useItemFormContext();
|
||||
|
||||
@@ -23,7 +22,7 @@ export default function ItemFormFloatingActions() {
|
||||
|
||||
// Handle cancel button click.
|
||||
const handleCancelBtnClick = (event) => {
|
||||
history.goBack();
|
||||
saveInvoke(onCancel, event);
|
||||
};
|
||||
|
||||
// Handle submit button click.
|
||||
@@ -38,7 +37,7 @@ export default function ItemFormFloatingActions() {
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
||||
<Button
|
||||
<SaveButton
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
@@ -47,7 +46,7 @@ export default function ItemFormFloatingActions() {
|
||||
className={'btn--submit'}
|
||||
>
|
||||
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
|
||||
</Button>
|
||||
</SaveButton>
|
||||
|
||||
<Button
|
||||
className={classNames('ml1', 'btn--submit-new')}
|
||||
@@ -82,3 +81,7 @@ export default function ItemFormFloatingActions() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SaveButton = styled(Button)`
|
||||
min-width: 100px;
|
||||
`;
|
||||
112
src/containers/Items/ItemFormFormik.js
Normal file
112
src/containers/Items/ItemFormFormik.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ItemFormProvider } from './ItemFormProvider';
|
||||
import DashboardCard from 'components/Dashboard/DashboardCard';
|
||||
import ItemForm from 'containers/Items/ItemForm';
|
||||
import ItemForm from './ItemForm';
|
||||
|
||||
/**
|
||||
* Item form page.
|
||||
@@ -12,11 +10,5 @@ export default function ItemFormPage() {
|
||||
const { id } = useParams();
|
||||
const idInteger = parseInt(id, 10);
|
||||
|
||||
return (
|
||||
<ItemFormProvider itemId={idInteger}>
|
||||
<DashboardCard page>
|
||||
<ItemForm />
|
||||
</DashboardCard>
|
||||
</ItemFormProvider>
|
||||
);
|
||||
}
|
||||
return <ItemForm itemId={idInteger} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React, { useEffect, createContext, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import {
|
||||
useItem,
|
||||
useSettingsItems,
|
||||
@@ -11,7 +8,6 @@ import {
|
||||
useEditItem,
|
||||
useAccounts,
|
||||
} from 'hooks/query';
|
||||
import { useDashboardPageTitle } from 'hooks/state';
|
||||
import { useWatchItemError } from './utils';
|
||||
|
||||
const ItemFormContext = createContext();
|
||||
@@ -59,6 +55,13 @@ function ItemFormProvider({ itemId, ...props }) {
|
||||
// Detarmines whether the form new mode.
|
||||
const isNewMode = duplicateId || !itemId;
|
||||
|
||||
// Detarmines the form loading state.
|
||||
const isFormLoading =
|
||||
isItemsSettingsLoading ||
|
||||
isAccountsLoading ||
|
||||
isItemsCategoriesLoading ||
|
||||
isItemLoading;
|
||||
|
||||
// Provider state.
|
||||
const provider = {
|
||||
itemId,
|
||||
@@ -68,6 +71,7 @@ function ItemFormProvider({ itemId, ...props }) {
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
|
||||
isFormLoading,
|
||||
isAccountsLoading,
|
||||
isItemsCategoriesLoading,
|
||||
isItemLoading,
|
||||
@@ -77,27 +81,7 @@ function ItemFormProvider({ itemId, ...props }) {
|
||||
setSubmitPayload,
|
||||
};
|
||||
|
||||
// Change page title dispatcher.
|
||||
const changePageTitle = useDashboardPageTitle();
|
||||
|
||||
// Changes the page title in new and edit mode.
|
||||
useEffect(() => {
|
||||
isNewMode
|
||||
? changePageTitle(intl.get('new_item'))
|
||||
: changePageTitle(intl.get('edit_item_details'));
|
||||
}, [changePageTitle, isNewMode]);
|
||||
|
||||
const loading =
|
||||
isItemsSettingsLoading ||
|
||||
isAccountsLoading ||
|
||||
isItemsCategoriesLoading ||
|
||||
isItemLoading;
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={loading} name={'item-form'}>
|
||||
<ItemFormContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
return <ItemFormContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useItemFormContext = () => React.useContext(ItemFormContext);
|
||||
|
||||
@@ -33,7 +33,7 @@ const defaultInitialValues = {
|
||||
/**
|
||||
* Initial values in create and edit mode.
|
||||
*/
|
||||
export const useItemFormInitialValues = (item) => {
|
||||
export const useItemFormInitialValues = (item, initialValues) => {
|
||||
const { items: itemsSettings } = useSettingsSelector();
|
||||
|
||||
return useMemo(
|
||||
@@ -54,8 +54,9 @@ export const useItemFormInitialValues = (item) => {
|
||||
transformItemFormData(item, defaultInitialValues),
|
||||
defaultInitialValues,
|
||||
),
|
||||
...initialValues,
|
||||
}),
|
||||
[item, itemsSettings],
|
||||
[item, itemsSettings, initialValues],
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user