feat: quick create action on select/suggest items fields.

This commit is contained in:
a.bouhuolia
2021-11-10 20:49:50 +02:00
parent d8e9be0246
commit da67217d74
61 changed files with 1885 additions and 745 deletions

View File

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

View File

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

View File

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

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 { 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} />;
}

View File

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

View File

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