Exit dirty form (#1816)

* Add Exit form prompt component and change some minor styles in other components to match

* Add Exit form prompt provider

* Adjust generic form and useform hook to allow using exit form prompt provider

* Add exit form prompt provider to index

* wip

* Fix types

* Fix styling

* Fix types

* Revert warehouse details refactor

* Add handling of edge cases to exit prompt

* Refactor, add comments, fix some types

* Refactor after exit form dialog name change

* fix types

* Fixes after review

* Add default value for useform prop opts so the app doesn't crash

* Add missing category prop to getting initial data for category details form

* Add exit dialog to everywhere WIP (#1600)

* Add Exit form prompt component and change some minor styles in other components to match

* Add Exit form prompt provider

* Adjust generic form and useform hook to allow using exit form prompt provider

* Add exit form prompt provider to index

* wip

* Fix types

* Fix styling

* Fix types

* Revert warehouse details refactor

* Add handling of edge cases to exit prompt

* Refactor, add comments, fix some types

* Refactor after exit form dialog name change

* fix types

* Add CommonUseFormResultWithHandlers type for later use and refactor handleFormSubmit util

* Refactor login form not to use custom form since it doesn't need to

* Add exit form dialog to order refund page

* Add exit form dialog to order return page

* Add exit form dialog to order order settings  page

* Add exit form dialog to product variant page

* Add exit form dialog to product create page

* Add exit form dialog to product update page

* Add exit form dialog to product variant create page

* Fix confirm leave prop passing in generic Form

* Add util function to handle for submit to extract errors

* Add confirmLeave prop to generic forms

* Move handleChange for custom forms to useForm

* Add exit dialog to more forms

* Add extract mutation errors util function

* Add extracting errors to submit functions that use metadata create handler

* Fix typo

* Add missing category prop to getting initial data for category details form

* Fix types

* wip

* wip

* wip

* wip

* Fix types & refactor

* Fix types & refactor

* Fix typescript

* Fix unmatching tag

* Fixes

* Add handling of multiple forms at once to exit dirty form provider

* Change all usages of ExitFormDialogContext to designated hook

* wip

* wip

* wip

* Fix types wip

* Fix types

* Remove console logs

* Add isSubmitting prop to exit form dialog in order to avoid enabling exit dialog while submit is still in progresS

* Replace handleSubmit global util with a hook to use exit form dialog props inside

* Move useHandleSubmit to general hooks dir, update imports

* Small fixes

* Update snapshots

* Fix types

* Small fixes due to extensive rebase

* Update package lock

* Fixes after rebase

* Remove exit form from customer address dialog

* Fix types and update messages

* Fix types

* Change imports names

* Refactor

* Remove unnecessary console.log

* Update types, snapshots. etc after rebase
This commit is contained in:
Magdalena Markusik 2022-02-01 10:58:06 +01:00 committed by GitHub
parent 9b1fce078a
commit 3d636f4789
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
185 changed files with 3214 additions and 1852 deletions

View file

@ -2194,6 +2194,22 @@
"context": "filters error messages value required", "context": "filters error messages value required",
"string": "Choose a value" "string": "Choose a value"
}, },
"src_dot_components_dot_Form_dot_cancelButton": {
"context": "ExitFormPrompt cancel button",
"string": "leave without saving"
},
"src_dot_components_dot_Form_dot_confirmButton": {
"context": "ExitFormPrompt confirm button",
"string": "save & continue"
},
"src_dot_components_dot_Form_dot_description": {
"context": "ExitFormPrompt description",
"string": "You have unsaved changes on this view. What would you like to do with them?"
},
"src_dot_components_dot_Form_dot_title": {
"context": "ExitFormPrompt title",
"string": "Are you sure you want to leave?"
},
"src_dot_components_dot_ImageUpload_dot_1731007575": { "src_dot_components_dot_ImageUpload_dot_1731007575": {
"context": "image upload", "context": "image upload",
"string": "Drop here to upload" "string": "Drop here to upload"
@ -5382,20 +5398,12 @@
"src_dot_orders_dot_views_dot_OrderDetails_dot_4085755992": { "src_dot_orders_dot_views_dot_OrderDetails_dot_4085755992": {
"string": "Invoice email sent" "string": "Invoice email sent"
}, },
"src_dot_orders_dot_views_dot_OrderDetails_dot_4207717971": {
"context": "snackbar title",
"string": "Could not generate invoice"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_55607988": { "src_dot_orders_dot_views_dot_OrderDetails_dot_55607988": {
"string": "Invoice is Generating" "string": "Invoice is Generating"
}, },
"src_dot_orders_dot_views_dot_OrderDetails_dot_617145655": { "src_dot_orders_dot_views_dot_OrderDetails_dot_617145655": {
"string": "Shipping method successfully updated" "string": "Shipping method successfully updated"
}, },
"src_dot_orders_dot_views_dot_OrderDetails_dot_811176136": {
"context": "error message",
"string": "No invoice plugin installed"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_927945225": { "src_dot_orders_dot_views_dot_OrderDetails_dot_927945225": {
"string": "Fulfillment successfully cancelled" "string": "Fulfillment successfully cancelled"
}, },
@ -8371,6 +8379,18 @@
"context": "section header", "context": "section header",
"string": "Webhook Status" "string": "Webhook Status"
}, },
"src_dot_webhooks_dot_components_dot_WebhooksDetailsPage_dot_header": {
"context": "header",
"string": "Unnamed Webhook Details"
},
"src_dot_webhooks_dot_components_dot_WebhooksDetailsPage_dot_headerCreate": {
"context": "header",
"string": "Create Webhook"
},
"src_dot_webhooks_dot_components_dot_WebhooksDetailsPage_dot_headerNamed": {
"context": "header",
"string": "{webhookName} Details"
},
"src_dot_webhooks_dot_components_dot_WebhooksList_dot_1153324159": { "src_dot_webhooks_dot_components_dot_WebhooksList_dot_1153324159": {
"string": "No webhooks found" "string": "No webhooks found"
}, },

View file

@ -1,11 +1,13 @@
import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg"; import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg";
import plusIcon from "@assets/images/plus-icon.svg"; import plusIcon from "@assets/images/plus-icon.svg";
import { Card, CardContent, Grid, Typography } from "@material-ui/core"; import { Card, CardContent, Grid, Typography } from "@material-ui/core";
import { AppInstall_appInstall_errors } from "@saleor/apps/types/AppInstall";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { Button } from "@saleor/macaw-ui"; import { Button } from "@saleor/macaw-ui";
import classNames from "classnames"; import classNames from "classnames";
@ -19,7 +21,7 @@ export interface AppInstallPageProps {
data: AppFetch_appFetchManifest_manifest; data: AppFetch_appFetchManifest_manifest;
loading: boolean; loading: boolean;
navigateToAppsList: () => void; navigateToAppsList: () => void;
onSubmit: () => void; onSubmit: () => SubmitPromise<AppInstall_appInstall_errors[]>;
} }
export const AppInstallPage: React.FC<AppInstallPageProps> = ({ export const AppInstallPage: React.FC<AppInstallPageProps> = ({

View file

@ -6,6 +6,7 @@ import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { AppErrorFragment } from "@saleor/fragments/types/AppErrorFragment"; import { AppErrorFragment } from "@saleor/fragments/types/AppErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -28,7 +29,9 @@ export interface CustomAppCreatePageProps {
permissions: ShopInfo_shop_permissions[]; permissions: ShopInfo_shop_permissions[];
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit: (data: CustomAppCreatePageFormData) => void; onSubmit: (
data: CustomAppCreatePageFormData
) => SubmitPromise<AppErrorFragment[]>;
} }
const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => { const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
@ -52,7 +55,7 @@ const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
const permissionsError = getAppErrorMessage(formErrors.permissions, intl); const permissionsError = getAppErrorMessage(formErrors.permissions, intl);
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ data, change, hasChanged, submit }) => ( {({ data, change, hasChanged, submit }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>

View file

@ -44,7 +44,9 @@ export interface CustomAppDetailsPageProps {
onTokenDelete: (id: string) => void; onTokenDelete: (id: string) => void;
onTokenClose: () => void; onTokenClose: () => void;
onTokenCreate: () => void; onTokenCreate: () => void;
onSubmit: (data: CustomAppDetailsPageFormData) => SubmitPromise; onSubmit: (
data: CustomAppDetailsPageFormData
) => SubmitPromise<AppErrorFragment[]>;
onWebhookCreate: () => void; onWebhookCreate: () => void;
onWebhookRemove: (id: string) => void; onWebhookRemove: (id: string) => void;
navigateToWebhookDetails: (id: string) => () => void; navigateToWebhookDetails: (id: string) => () => void;
@ -94,7 +96,7 @@ const CustomAppDetailsPage: React.FC<CustomAppDetailsPageProps> = props => {
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ data, change, hasChanged, submit }) => ( {({ data, change, hasChanged, submit }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>

View file

@ -12,6 +12,7 @@ import CardSpacer from "@saleor/components/CardSpacer";
import ConfirmButton from "@saleor/components/ConfirmButton"; import ConfirmButton from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { Button, ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Button, ConfirmButtonTransitionState } from "@saleor/macaw-ui";
@ -25,7 +26,7 @@ export interface TokenCreateDialogProps {
open: boolean; open: boolean;
token: string | undefined; token: string | undefined;
onClose: () => void; onClose: () => void;
onCreate: (name: string) => void; onCreate: (name: string) => SubmitPromise;
} }
type TokenCreateStep = "form" | "summary"; type TokenCreateStep = "form" | "summary";

View file

@ -2,6 +2,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { extractMutationErrors } from "@saleor/misc";
import getAppErrorMessage from "@saleor/utils/errors/app"; import getAppErrorMessage from "@saleor/utils/errors/app";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -67,15 +68,19 @@ export const InstallAppCreate: React.FC<InstallAppCreateProps> = ({
const handleSubmit = () => { const handleSubmit = () => {
const manifest = fetchManifestOpts?.data?.appFetchManifest?.manifest; const manifest = fetchManifestOpts?.data?.appFetchManifest?.manifest;
return extractMutationErrors(
installApp({ installApp({
variables: { variables: {
input: { input: {
appName: manifest?.name, appName: manifest?.name,
manifestUrl, manifestUrl,
permissions: manifest?.permissions.map(permission => permission.code) permissions: manifest?.permissions.map(
permission => permission.code
)
} }
} }
}); })
);
}; };
useEffect(() => { useEffect(() => {

View file

@ -3,6 +3,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -41,7 +42,8 @@ export const CustomAppCreate: React.FC<CustomAppCreateProps> = ({
onCompleted: onSubmit onCompleted: onSubmit
}); });
const handleSubmit = (data: CustomAppCreatePageFormData) => const handleSubmit = async (data: CustomAppCreatePageFormData) =>
extractMutationErrors(
createApp({ createApp({
variables: { variables: {
input: { input: {
@ -51,7 +53,8 @@ export const CustomAppCreate: React.FC<CustomAppCreateProps> = ({
: data.permissions : data.permissions
} }
} }
}); })
);
return ( return (
<> <>

View file

@ -9,7 +9,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getStringOrPlaceholder } from "@saleor/misc"; import { extractMutationErrors, getStringOrPlaceholder } from "@saleor/misc";
import getAppErrorMessage from "@saleor/utils/errors/app"; import getAppErrorMessage from "@saleor/utils/errors/app";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog"; import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog";
@ -181,8 +181,9 @@ export const CustomAppDetails: React.FC<OrderListProps> = ({
onCompleted: onTokenDelete onCompleted: onTokenDelete
}); });
const handleSubmit = async (data: CustomAppDetailsPageFormData) => { const handleSubmit = async (data: CustomAppDetailsPageFormData) =>
const result = await updateApp({ extractMutationErrors(
updateApp({
variables: { variables: {
id, id,
input: { input: {
@ -192,10 +193,8 @@ export const CustomAppDetails: React.FC<OrderListProps> = ({
: data.permissions : data.permissions
} }
} }
}); })
);
return result.data.appUpdate.errors;
};
const handleTokenCreate = (name: string) => const handleTokenCreate = (name: string) =>
createToken({ createToken({

View file

@ -11,6 +11,7 @@ import Savebar from "@saleor/components/Savebar";
import { ListSettingsUpdate } from "@saleor/components/TablePagination"; import { ListSettingsUpdate } from "@saleor/components/TablePagination";
import { AttributeDetailsFragment } from "@saleor/fragments/types/AttributeDetailsFragment"; import { AttributeDetailsFragment } from "@saleor/fragments/types/AttributeDetailsFragment";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -41,7 +42,7 @@ export interface AttributePageProps {
values: AttributeDetails_attribute_choices; values: AttributeDetails_attribute_choices;
onBack: () => void; onBack: () => void;
onDelete: () => void; onDelete: () => void;
onSubmit: (data: AttributePageFormData) => void; onSubmit: (data: AttributePageFormData) => SubmitPromise;
onValueAdd: () => void; onValueAdd: () => void;
onValueDelete: (id: string) => void; onValueDelete: (id: string) => void;
onValueReorder: ReorderAction; onValueReorder: ReorderAction;
@ -156,7 +157,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
}; };
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <Form confirmLeave initial={initialForm} onSubmit={handleSubmit}>
{({ {({
change, change,
set, set,

View file

@ -3,7 +3,7 @@ import useListSettings from "@saleor/hooks/useListSettings";
import useLocalPageInfo, { getMaxPage } from "@saleor/hooks/useLocalPageInfo"; import useLocalPageInfo, { getMaxPage } from "@saleor/hooks/useLocalPageInfo";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getStringOrPlaceholder } from "@saleor/misc"; import { getMutationErrors, getStringOrPlaceholder } from "@saleor/misc";
import { ListViews, ReorderEvent } from "@saleor/types"; import { ListViews, ReorderEvent } from "@saleor/types";
import { AttributeErrorCode } from "@saleor/types/globalTypes"; import { AttributeErrorCode } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
@ -154,16 +154,18 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
); );
const handleCreate = async (data: AttributePageFormData) => { const handleCreate = async (data: AttributePageFormData) => {
const input = getAttributeData(data, values);
const result = await attributeCreate({ const result = await attributeCreate({
variables: { variables: {
input input: getAttributeData(data, values)
} }
}); });
return result.data.attributeCreate?.attribute?.id || null; return {
id: result.data.attributeCreate?.attribute?.id || null,
errors: getMutationErrors(result)
}; };
};
const handleSubmit = createMetadataCreateHandler( const handleSubmit = createMetadataCreateHandler(
handleCreate, handleCreate,
updateMetadata, updateMetadata,

View file

@ -6,6 +6,7 @@ import useLocalPaginator, {
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import { ListViews, ReorderEvent } from "@saleor/types"; import { ListViews, ReorderEvent } from "@saleor/types";
import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
@ -15,6 +16,7 @@ import {
useMetadataUpdate, useMetadataUpdate,
usePrivateMetadataUpdate usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata"; } from "@saleor/utils/metadata/updateMetadata";
import omit from "lodash/omit";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -209,25 +211,23 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
} }
}); });
const handleUpdate = async (data: AttributePageFormData) => { const handleUpdate = async (data: AttributePageFormData) =>
const input = { extractMutationErrors(
...data, attributeUpdate({
entityType: undefined,
inputType: undefined,
metadata: undefined,
privateMetadata: undefined,
storefrontSearchPosition: parseInt(data?.storefrontSearchPosition, 0)
};
const result = await attributeUpdate({
variables: { variables: {
id, id,
input input: {
...omit(data, [
"entityType",
"inputType",
"metadata",
"privateMetadata"
]),
storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 0)
} }
}); }
})
return result.data?.attributeUpdate.errors; );
};
const handleSubmit = createMetadataUpdateHandler( const handleSubmit = createMetadataUpdateHandler(
data?.attribute, data?.attribute,

View file

@ -1,6 +1,6 @@
import { DEMO_MODE } from "@saleor/config"; import { DEMO_MODE } from "@saleor/config";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import React from "react"; import React from "react";
export interface LoginFormData { export interface LoginFormData {
@ -12,7 +12,7 @@ export interface UseLoginFormResult {
change: FormChange; change: FormChange;
data: LoginFormData; data: LoginFormData;
hasChanged: boolean; hasChanged: boolean;
submit: () => Promise<boolean>; submit: () => SubmitPromise;
} }
export interface LoginFormProps { export interface LoginFormProps {
@ -33,32 +33,18 @@ const getLoginFormData = () => {
function useLoginForm( function useLoginForm(
onSubmit: (data: LoginFormData) => SubmitPromise onSubmit: (data: LoginFormData) => SubmitPromise
): UseLoginFormResult { ): UseLoginFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm(getLoginFormData()); const form = useForm(getLoginFormData());
const handleChange: FormChange = (event, cb) => { const { change, hasChanged, data, setChanged } = form;
form.change(event, cb);
triggerChange();
};
const data: LoginFormData = { const handleFormSubmit = useHandleFormSubmit({ onSubmit, setChanged });
...form.data
};
const handleSubmit = async (data: LoginFormData) => { const submit = async () => handleFormSubmit(data);
const errors = await onSubmit(data);
return errors;
};
const submit = async () => handleFormSubmit(data, handleSubmit, setChanged);
return { return {
change: handleChange, change,
data, data,
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -0,0 +1,4 @@
export interface LoginFormData {
email: string;
password: string;
}

View file

@ -1,6 +1,7 @@
import { TextField, Typography } from "@material-ui/core"; import { TextField, Typography } from "@material-ui/core";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { Button } from "@saleor/macaw-ui"; import { Button } from "@saleor/macaw-ui";
import { SetPasswordData } from "@saleor/sdk"; import { SetPasswordData } from "@saleor/sdk";
import getAccountErrorMessage from "@saleor/utils/errors/account"; import getAccountErrorMessage from "@saleor/utils/errors/account";
@ -16,7 +17,7 @@ export interface NewPasswordPageFormData {
export interface NewPasswordPageProps { export interface NewPasswordPageProps {
disabled: boolean; disabled: boolean;
errors: SetPasswordData["errors"]; errors: SetPasswordData["errors"];
onSubmit: (data: NewPasswordPageFormData) => void; onSubmit: (data: NewPasswordPageFormData) => SubmitPromise;
} }
const initialForm: NewPasswordPageFormData = { const initialForm: NewPasswordPageFormData = {

View file

@ -1,6 +1,8 @@
import { TextField, Typography } from "@material-ui/core"; import { TextField, Typography } from "@material-ui/core";
import { RequestPasswordReset_requestPasswordReset_errors } from "@saleor/auth/types/RequestPasswordReset";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { ArrowRightIcon, Button, IconButton } from "@saleor/macaw-ui"; import { ArrowRightIcon, Button, IconButton } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
@ -15,7 +17,9 @@ export interface ResetPasswordPageProps {
disabled: boolean; disabled: boolean;
error: string; error: string;
onBack: () => void; onBack: () => void;
onSubmit: (data: ResetPasswordPageFormData) => void; onSubmit: (
data: ResetPasswordPageFormData
) => SubmitPromise<RequestPasswordReset_requestPasswordReset_errors[]>;
} }
const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => { const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {

View file

@ -8,7 +8,7 @@ import useRouter from "use-react-router";
import { useUser } from ".."; import { useUser } from "..";
import LoginPage from "../components/LoginPage"; import LoginPage from "../components/LoginPage";
import { LoginFormData } from "../components/LoginPage/form"; import { LoginFormData } from "../components/LoginPage/types";
import { availableExternalAuthentications } from "../queries"; import { availableExternalAuthentications } from "../queries";
import { AvailableExternalAuthentications } from "../types/AvailableExternalAuthentications"; import { AvailableExternalAuthentications } from "../types/AvailableExternalAuthentications";
import { import {

View file

@ -1,6 +1,7 @@
import { APP_MOUNT_URI } from "@saleor/config"; import { APP_MOUNT_URI } from "@saleor/config";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import urlJoin from "url-join"; import urlJoin from "url-join";
@ -38,6 +39,7 @@ const ResetPasswordView: React.FC = () => {
<RequestPasswordResetMutation onCompleted={handleRequestPasswordReset}> <RequestPasswordResetMutation onCompleted={handleRequestPasswordReset}>
{(requestPasswordReset, requestPasswordResetOpts) => { {(requestPasswordReset, requestPasswordResetOpts) => {
const handleSubmit = (data: ResetPasswordPageFormData) => const handleSubmit = (data: ResetPasswordPageFormData) =>
extractMutationErrors(
requestPasswordReset({ requestPasswordReset({
variables: { variables: {
email: data.email, email: data.email,
@ -47,7 +49,8 @@ const ResetPasswordView: React.FC = () => {
newPasswordUrl().replace(/\?/, "") newPasswordUrl().replace(/\?/, "")
) )
} }
}); })
);
return ( return (
<ResetPasswordPage <ResetPasswordPage

View file

@ -1,11 +1,15 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, {
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; CommonUseFormResult,
FormChange
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React, { useEffect } from "react";
export interface CategoryCreateFormData extends MetadataFormData { export interface CategoryCreateFormData extends MetadataFormData {
name: string; name: string;
@ -21,12 +25,9 @@ interface CategoryCreateHandlers {
changeMetadata: FormChange; changeMetadata: FormChange;
changeDescription: RichTextEditorChange; changeDescription: RichTextEditorChange;
} }
export interface UseCategoryCreateFormResult { export interface UseCategoryCreateFormResult
change: FormChange; extends CommonUseFormResult<CategoryCreateData> {
data: CategoryCreateData;
handlers: CategoryCreateHandlers; handlers: CategoryCreateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
} }
export interface CategoryCreateFormProps { export interface CategoryCreateFormProps {
@ -34,20 +35,37 @@ export interface CategoryCreateFormProps {
onSubmit: (data: CategoryCreateData) => Promise<any[]>; onSubmit: (data: CategoryCreateData) => Promise<any[]>;
} }
function useCategoryCreateForm( const initialData: CategoryCreateFormData = {
onSubmit: (data: CategoryCreateData) => Promise<any[]>
): UseCategoryCreateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CategoryCreateFormData>({
metadata: [], metadata: [],
name: "", name: "",
privateMetadata: [], privateMetadata: [],
seoDescription: "", seoDescription: "",
seoTitle: "", seoTitle: "",
slug: "" slug: ""
};
function useCategoryCreateForm(
onSubmit: (data: CategoryCreateData) => Promise<any[]>
): UseCategoryCreateFormResult {
const {
handleChange,
data,
hasChanged,
triggerChange,
setChanged,
formId
} = useForm(initialData, undefined, { confirmLeave: true });
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
}); });
const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const [description, changeDescription] = useRichText({ const [description, changeDescription] = useRichText({
initial: null, initial: null,
triggerChange triggerChange
@ -57,19 +75,17 @@ function useCategoryCreateForm(
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date // Need to make it function to always have description.current up to date
const getData = (): CategoryCreateData => ({ const getData = (): CategoryCreateData => ({
...form.data, ...data,
description: description.current description: description.current
}); });
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); const submit = () => handleFormSubmit(getData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
return { return {
change: handleChange, change: handleChange,
@ -78,7 +94,7 @@ function useCategoryCreateForm(
changeDescription, changeDescription,
changeMetadata changeMetadata
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -1,14 +1,18 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { CategoryDetails_category } from "@saleor/categories/types/CategoryDetails"; import { CategoryDetails_category } from "@saleor/categories/types/CategoryDetails";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, {
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; CommonUseFormResult,
FormChange
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React, { useEffect } from "react";
export interface CategoryUpdateFormData extends MetadataFormData { export interface CategoryUpdateFormData extends MetadataFormData {
backgroundImageAlt: string; backgroundImageAlt: string;
@ -25,12 +29,9 @@ interface CategoryUpdateHandlers {
changeMetadata: FormChange; changeMetadata: FormChange;
changeDescription: RichTextEditorChange; changeDescription: RichTextEditorChange;
} }
export interface UseCategoryUpdateFormResult { export interface UseCategoryUpdateFormResult
change: FormChange; extends CommonUseFormResult<CategoryUpdateData> {
data: CategoryUpdateData;
handlers: CategoryUpdateHandlers; handlers: CategoryUpdateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
} }
export interface CategoryUpdateFormProps { export interface CategoryUpdateFormProps {
@ -39,14 +40,7 @@ export interface CategoryUpdateFormProps {
onSubmit: (data: CategoryUpdateData) => Promise<any[]>; onSubmit: (data: CategoryUpdateData) => Promise<any[]>;
} }
function useCategoryUpdateForm( const getInitialData = (category?: CategoryDetails_category) => ({
category: CategoryDetails_category,
onSubmit: (data: CategoryUpdateData) => Promise<any[]>
): UseCategoryUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CategoryUpdateFormData>({
backgroundImageAlt: category?.backgroundImage?.alt || "", backgroundImageAlt: category?.backgroundImage?.alt || "",
metadata: category?.metadata?.map(mapMetadataItemToInput), metadata: category?.metadata?.map(mapMetadataItemToInput),
name: category?.name || "", name: category?.name || "",
@ -55,6 +49,30 @@ function useCategoryUpdateForm(
seoTitle: category?.seoTitle || "", seoTitle: category?.seoTitle || "",
slug: category?.slug || "" slug: category?.slug || ""
}); });
function useCategoryUpdateForm(
category: CategoryDetails_category,
onSubmit: (data: CategoryUpdateData) => Promise<any[]>
): UseCategoryUpdateFormResult {
const {
handleChange,
data,
triggerChange,
hasChanged,
setChanged,
formId
} = useForm(getInitialData(category), undefined, { confirmLeave: true });
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const [description, changeDescription] = useRichText({ const [description, changeDescription] = useRichText({
initial: category?.description, initial: category?.description,
triggerChange triggerChange
@ -66,24 +84,22 @@ function useCategoryUpdateForm(
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date // Need to make it function to always have description.current up to date
const getData = (): CategoryUpdateData => ({ const getData = (): CategoryUpdateData => ({
...form.data, ...data,
description: description.current description: description.current
}); });
const getSubmitData = (): CategoryUpdateData => ({ const getSubmitData = (): CategoryUpdateData => ({
...getData(), ...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified) ...getMetadata(data, isMetadataModified, isPrivateMetadataModified)
}); });
const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); const submit = () => handleFormSubmit(getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
return { return {
change: handleChange, change: handleChange,
@ -92,7 +108,7 @@ function useCategoryUpdateForm(
changeDescription, changeDescription,
changeMetadata changeMetadata
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -1,6 +1,7 @@
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
useMetadataUpdate, useMetadataUpdate,
@ -61,8 +62,12 @@ export const CategoryCreateView: React.FC<CategoryCreateViewProps> = ({
} }
}); });
return result.data?.categoryCreate.category?.id || null; return {
id: result.data?.categoryCreate.category?.id || null,
errors: getMutationErrors(result)
}; };
};
const handleSubmit = createMetadataCreateHandler( const handleSubmit = createMetadataCreateHandler(
handleCreate, handleCreate,
updateMetadata, updateMetadata,

View file

@ -22,7 +22,7 @@ import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { PAGINATE_BY } from "../../config"; import { PAGINATE_BY } from "../../config";
import { maybe } from "../../misc"; import { extractMutationErrors, maybe } from "../../misc";
import { useProductBulkDeleteMutation } from "../../products/mutations"; import { useProductBulkDeleteMutation } from "../../products/mutations";
import { productAddUrl, productUrl } from "../../products/urls"; import { productAddUrl, productUrl } from "../../products/urls";
import { CategoryInput } from "../../types/globalTypes"; import { CategoryInput } from "../../types/globalTypes";
@ -178,8 +178,9 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
paginationState paginationState
); );
const handleUpdate = async (formData: CategoryUpdateData) => { const handleUpdate = async (formData: CategoryUpdateData) =>
const result = await updateCategory({ extractMutationErrors(
updateCategory({
variables: { variables: {
id, id,
input: { input: {
@ -193,10 +194,9 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
slug: formData.slug slug: formData.slug
} }
} }
}); })
);
return result.data.categoryUpdate.errors;
};
const handleSubmit = createMetadataUpdateHandler( const handleSubmit = createMetadataUpdateHandler(
data?.category, data?.category,
handleUpdate, handleUpdate,

View file

@ -1,4 +1,5 @@
import { countries } from "@saleor/fixtures"; import { countries } from "@saleor/fixtures";
import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
import Decorator from "@saleor/storybook/Decorator"; import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
@ -8,7 +9,7 @@ import ChannelDetailsPage, {
ChannelDetailsPageProps ChannelDetailsPageProps
} from "./ChannelDetailsPage"; } from "./ChannelDetailsPage";
const props: ChannelDetailsPageProps = { const props: ChannelDetailsPageProps<ChannelErrorFragment[]> = {
currencyCodes: [ currencyCodes: [
{ label: "USD", value: "USD" }, { label: "USD", value: "USD" },
{ label: "PLN", value: "PLN" } { label: "PLN", value: "PLN" }

View file

@ -8,6 +8,7 @@ import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragme
import { CountryFragment } from "@saleor/fragments/types/CountryFragment"; import { CountryFragment } from "@saleor/fragments/types/CountryFragment";
import { SearchData } from "@saleor/hooks/makeTopLevelSearch"; import { SearchData } from "@saleor/hooks/makeTopLevelSearch";
import { getParsedSearchData } from "@saleor/hooks/makeTopLevelSearch/utils"; import { getParsedSearchData } from "@saleor/hooks/makeTopLevelSearch/utils";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { import {
@ -27,7 +28,7 @@ import { Channel_channel } from "../../types/Channel";
import { ChannelShippingZones } from "./types"; import { ChannelShippingZones } from "./types";
import { getUpdatedIdsWithNewId, getUpdatedIdsWithoutNewId } from "./utils"; import { getUpdatedIdsWithNewId, getUpdatedIdsWithoutNewId } from "./utils";
export interface ChannelDetailsPageProps { export interface ChannelDetailsPageProps<TErrors> {
channel?: Channel_channel; channel?: Channel_channel;
currencyCodes?: SingleAutocompleteChoiceType[]; currencyCodes?: SingleAutocompleteChoiceType[];
disabled: boolean; disabled: boolean;
@ -40,12 +41,12 @@ export interface ChannelDetailsPageProps {
countries: CountryFragment[]; countries: CountryFragment[];
onBack?: () => void; onBack?: () => void;
onDelete?: () => void; onDelete?: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: FormData) => SubmitPromise<TErrors[]>;
updateChannelStatus?: () => void; updateChannelStatus?: () => void;
searchShippingZones: (query: string) => void; searchShippingZones: (query: string) => void;
} }
export const ChannelDetailsPage: React.FC<ChannelDetailsPageProps> = ({ const ChannelDetailsPage = function<TErrors>({
channel, channel,
currencyCodes, currencyCodes,
disabled, disabled,
@ -61,7 +62,7 @@ export const ChannelDetailsPage: React.FC<ChannelDetailsPageProps> = ({
fetchMoreShippingZones, fetchMoreShippingZones,
countries, countries,
channelShippingZones = [] channelShippingZones = []
}) => { }: ChannelDetailsPageProps<TErrors>) {
const [selectedCurrencyCode, setSelectedCurrencyCode] = useState(""); const [selectedCurrencyCode, setSelectedCurrencyCode] = useState("");
const [ const [
selectedCountryDisplayName, selectedCountryDisplayName,
@ -92,7 +93,7 @@ export const ChannelDetailsPage: React.FC<ChannelDetailsPageProps> = ({
); );
return ( return (
<Form onSubmit={onSubmit} initial={initialData}> <Form confirmLeave onSubmit={onSubmit} initial={initialData}>
{({ change, data, hasChanged, submit, set }) => { {({ change, data, hasChanged, submit, set }) => {
const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler( const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler(
change, change,

View file

@ -11,6 +11,7 @@ import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/ut
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
import { extractMutationErrors } from "@saleor/misc";
import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch"; import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch";
import currencyCodes from "currency-codes"; import currencyCodes from "currency-codes";
import React from "react"; import React from "react";
@ -44,6 +45,7 @@ export const ChannelCreateView = ({}) => {
currencyCode, currencyCode,
...rest ...rest
}: FormData) => }: FormData) =>
extractMutationErrors(
createChannel({ createChannel({
variables: { variables: {
input: { input: {
@ -52,7 +54,8 @@ export const ChannelCreateView = ({}) => {
addShippingZones: shippingZonesIdsToAdd addShippingZones: shippingZonesIdsToAdd
} }
} }
}); })
);
const { const {
loadMore: fetchMoreShippingZones, loadMore: fetchMoreShippingZones,

View file

@ -14,6 +14,7 @@ import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/ut
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
import { extractMutationErrors } from "@saleor/misc";
import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch"; import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch";
import { useChannelShippingZones } from "@saleor/shipping/queries"; import { useChannelShippingZones } from "@saleor/shipping/queries";
import getChannelsErrorMessage from "@saleor/utils/errors/channels"; import getChannelsErrorMessage from "@saleor/utils/errors/channels";
@ -105,6 +106,7 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
shippingZonesIdsToAdd, shippingZonesIdsToAdd,
defaultCountry defaultCountry
}: FormData) => }: FormData) =>
extractMutationErrors(
updateChannel({ updateChannel({
variables: { variables: {
id: data?.channel.id, id: data?.channel.id,
@ -116,7 +118,8 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
removeShippingZones: shippingZonesIdsToRemove removeShippingZones: shippingZonesIdsToRemove
} }
} }
}); })
);
const onDeleteCompleted = (data: ChannelDelete) => { const onDeleteCompleted = (data: ChannelDelete) => {
const errors = data.channelDelete.errors; const errors = data.channelDelete.errors;

View file

@ -1,13 +1,19 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { ChannelCollectionData } from "@saleor/channels/utils"; import { ChannelCollectionData } from "@saleor/channels/utils";
import { createChannelsChangeHandler } from "@saleor/collections/utils"; import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { COLLECTION_CREATE_FORM_ID } from "@saleor/collections/views/consts";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useForm, {
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; CommonUseFormResultWithHandlers,
FormChange,
SubmitPromise
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React, { useEffect } from "react";
export interface CollectionCreateFormData extends MetadataFormData { export interface CollectionCreateFormData extends MetadataFormData {
backgroundImage: { backgroundImage: {
@ -33,13 +39,10 @@ interface CollectionCreateHandlers {
data: Omit<ChannelCollectionData, "name" | "id"> data: Omit<ChannelCollectionData, "name" | "id">
) => void; ) => void;
} }
export interface UseCollectionCreateFormResult { export type UseCollectionCreateFormResult = CommonUseFormResultWithHandlers<
change: FormChange; CollectionCreateData,
data: CollectionCreateData; CollectionCreateHandlers
handlers: CollectionCreateHandlers; >;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CollectionCreateFormProps { export interface CollectionCreateFormProps {
currentChannels: ChannelCollectionData[]; currentChannels: ChannelCollectionData[];
@ -48,15 +51,9 @@ export interface CollectionCreateFormProps {
onSubmit: (data: CollectionCreateData) => SubmitPromise; onSubmit: (data: CollectionCreateData) => SubmitPromise;
} }
function useCollectionCreateForm( const getInitialData = (
currentChannels: ChannelCollectionData[], currentChannels: ChannelCollectionData[]
setChannels: (data: ChannelCollectionData[]) => void, ): CollectionCreateFormData => ({
onSubmit: (data: CollectionCreateData) => SubmitPromise
): UseCollectionCreateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CollectionCreateFormData>({
backgroundImage: { backgroundImage: {
url: null, url: null,
value: null value: null
@ -70,6 +67,34 @@ function useCollectionCreateForm(
seoTitle: "", seoTitle: "",
slug: "" slug: ""
}); });
function useCollectionCreateForm(
currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionCreateData) => SubmitPromise
): UseCollectionCreateFormResult {
const {
handleChange,
data: formData,
triggerChange,
setChanged,
hasChanged,
formId
} = useForm(getInitialData(currentChannels), undefined, {
confirmLeave: true,
formId: COLLECTION_CREATE_FORM_ID
});
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const [description, changeDescription] = useRichText({ const [description, changeDescription] = useRichText({
initial: null, initial: null,
triggerChange triggerChange
@ -79,15 +104,11 @@ function useCollectionCreateForm(
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date // Need to make it function to always have description.current up to date
const getData = (): CollectionCreateData => ({ const getData = (): CollectionCreateData => ({
...form.data, ...formData,
description: description.current description: description.current
}); });
@ -97,7 +118,9 @@ function useCollectionCreateForm(
triggerChange triggerChange
); );
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); const submit = () => handleFormSubmit(getData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
return { return {
change: handleChange, change: handleChange,
@ -107,7 +130,7 @@ function useCollectionCreateForm(
changeDescription, changeDescription,
changeMetadata changeMetadata
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -2,15 +2,20 @@ import { OutputData } from "@editorjs/editorjs";
import { ChannelCollectionData } from "@saleor/channels/utils"; import { ChannelCollectionData } from "@saleor/channels/utils";
import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails"; import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails";
import { createChannelsChangeHandler } from "@saleor/collections/utils"; import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { COLLECTION_DETAILS_FORM_ID } from "@saleor/collections/views/consts";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, {
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; CommonUseFormResultWithHandlers,
FormChange
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React, { useEffect } from "react";
export interface CollectionUpdateFormData extends MetadataFormData { export interface CollectionUpdateFormData extends MetadataFormData {
backgroundImageAlt: string; backgroundImageAlt: string;
@ -32,13 +37,10 @@ interface CollectionUpdateHandlers {
data: Omit<ChannelCollectionData, "name" | "id"> data: Omit<ChannelCollectionData, "name" | "id">
) => void; ) => void;
} }
export interface UseCollectionUpdateFormResult { export type UseCollectionUpdateFormResult = CommonUseFormResultWithHandlers<
change: FormChange; CollectionUpdateData,
data: CollectionUpdateData; CollectionUpdateHandlers
handlers: CollectionUpdateHandlers; >;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CollectionUpdateFormProps { export interface CollectionUpdateFormProps {
children: (props: UseCollectionUpdateFormResult) => React.ReactNode; children: (props: UseCollectionUpdateFormResult) => React.ReactNode;
@ -48,16 +50,10 @@ export interface CollectionUpdateFormProps {
onSubmit: (data: CollectionUpdateData) => Promise<any[]>; onSubmit: (data: CollectionUpdateData) => Promise<any[]>;
} }
function useCollectionUpdateForm( const getInitialData = (
collection: CollectionDetails_collection, collection: CollectionDetails_collection,
currentChannels: ChannelCollectionData[], currentChannels: ChannelCollectionData[]
setChannels: (data: ChannelCollectionData[]) => void, ): CollectionUpdateFormData => ({
onSubmit: (data: CollectionUpdateData) => Promise<any[]>
): UseCollectionUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CollectionUpdateFormData>({
backgroundImageAlt: collection?.backgroundImage?.alt || "", backgroundImageAlt: collection?.backgroundImage?.alt || "",
channelListings: currentChannels, channelListings: currentChannels,
metadata: collection?.metadata?.map(mapMetadataItemToInput), metadata: collection?.metadata?.map(mapMetadataItemToInput),
@ -67,6 +63,35 @@ function useCollectionUpdateForm(
seoTitle: collection?.seoTitle || "", seoTitle: collection?.seoTitle || "",
slug: collection?.slug || "" slug: collection?.slug || ""
}); });
function useCollectionUpdateForm(
collection: CollectionDetails_collection,
currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>
): UseCollectionUpdateFormResult {
const {
handleChange,
data: formData,
triggerChange,
setChanged,
hasChanged,
formId
} = useForm(getInitialData(collection, currentChannels), undefined, {
confirmLeave: true,
formId: COLLECTION_DETAILS_FORM_ID
});
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const { setExitDialogSubmitRef } = useExitFormDialog({
formId: COLLECTION_DETAILS_FORM_ID
});
const [description, changeDescription] = useRichText({ const [description, changeDescription] = useRichText({
initial: collection?.description, initial: collection?.description,
triggerChange triggerChange
@ -78,21 +103,17 @@ function useCollectionUpdateForm(
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date // Need to make it function to always have description.current up to date
const getData = (): CollectionUpdateData => ({ const getData = (): CollectionUpdateData => ({
...form.data, ...formData,
description: description.current description: description.current
}); });
const getSubmitData = (): CollectionUpdateData => ({ const getSubmitData = (): CollectionUpdateData => ({
...getData(), ...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified) ...getMetadata(formData, isMetadataModified, isPrivateMetadataModified)
}); });
const handleChannelChange = createChannelsChangeHandler( const handleChannelChange = createChannelsChangeHandler(
@ -101,7 +122,9 @@ function useCollectionUpdateForm(
triggerChange triggerChange
); );
const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); const submit = () => handleFormSubmit(getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
return { return {
change: handleChange, change: handleChange,
@ -111,7 +134,7 @@ function useCollectionUpdateForm(
changeDescription, changeDescription,
changeMetadata changeMetadata
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -7,6 +7,7 @@ import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getMutationErrors } from "@saleor/misc";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
@ -30,6 +31,7 @@ import {
collectionListUrl, collectionListUrl,
collectionUrl collectionUrl
} from "../urls"; } from "../urls";
import { COLLECTION_CREATE_FORM_ID } from "./consts";
interface CollectionCreateProps { interface CollectionCreateProps {
params: CollectionCreateUrlQueryParams; params: CollectionCreateUrlQueryParams;
@ -72,7 +74,12 @@ export const CollectionCreate: React.FC<CollectionCreateProps> = ({
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(allChannels, params?.action, { closeModal, openModal }); } = useChannels(
allChannels,
params?.action,
{ closeModal, openModal },
{ formId: COLLECTION_CREATE_FORM_ID }
);
const [createCollection, createCollectionOpts] = useCollectionCreateMutation({ const [createCollection, createCollectionOpts] = useCollectionCreateMutation({
onCompleted: data => { onCompleted: data => {
@ -130,7 +137,7 @@ export const CollectionCreate: React.FC<CollectionCreateProps> = ({
}); });
} }
return id; return { id, errors: getMutationErrors(result) };
}; };
const handleSubmit = createMetadataCreateHandler( const handleSubmit = createMetadataCreateHandler(

View file

@ -33,7 +33,7 @@ import { getParsedDataForJsonStringField } from "@saleor/utils/richText/misc";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { getMutationState, maybe } from "../../misc"; import { getMutationErrors, getMutationState, maybe } from "../../misc";
import { productUrl } from "../../products/urls"; import { productUrl } from "../../products/urls";
import { CollectionInput } from "../../types/globalTypes"; import { CollectionInput } from "../../types/globalTypes";
import CollectionDetailsPage from "../components/CollectionDetailsPage/CollectionDetailsPage"; import CollectionDetailsPage from "../components/CollectionDetailsPage/CollectionDetailsPage";
@ -53,6 +53,7 @@ import {
CollectionUrlDialog, CollectionUrlDialog,
CollectionUrlQueryParams CollectionUrlQueryParams
} from "../urls"; } from "../urls";
import { COLLECTION_DETAILS_FORM_ID } from "./consts";
interface CollectionDetailsProps { interface CollectionDetailsProps {
id: string; id: string;
@ -196,10 +197,15 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(collectionChannelsChoices, params?.action, { } = useChannels(
collectionChannelsChoices,
params?.action,
{
closeModal, closeModal,
openModal openModal
}); },
{ formId: COLLECTION_DETAILS_FORM_ID }
);
const handleUpdate = async (formData: CollectionUpdateData) => { const handleUpdate = async (formData: CollectionUpdateData) => {
const input: CollectionInput = { const input: CollectionInput = {
@ -242,8 +248,9 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
} }
}); });
return result.data.collectionUpdate.errors; return getMutationErrors(result);
}; };
const handleSubmit = createMetadataUpdateHandler( const handleSubmit = createMetadataUpdateHandler(
data?.collection, data?.collection,
handleUpdate, handleUpdate,

View file

@ -0,0 +1,2 @@
export const COLLECTION_DETAILS_FORM_ID = Symbol();
export const COLLECTION_CREATE_FORM_ID = Symbol();

View file

@ -9,6 +9,7 @@ interface CardTitleProps {
subtitle?: string | React.ReactNode; subtitle?: string | React.ReactNode;
toolbar?: React.ReactNode; toolbar?: React.ReactNode;
onClick?: (event: React.MouseEvent<any>) => void; onClick?: (event: React.MouseEvent<any>) => void;
onClose?: () => void;
} }
const CardTitle: React.FC<CardTitleProps> = ({ const CardTitle: React.FC<CardTitleProps> = ({

View file

@ -59,18 +59,20 @@ export const EditableTableCell: React.FC<EditableTableCellProps> = props => {
defaultValue, defaultValue,
focused, focused,
InputProps, InputProps,
value, value
onConfirm // onConfirm
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const handleConfirm = (data: { value: string }) => { // const handleConfirm = (data: { value: string }) => {
disable(); // disable();
onConfirm(data.value); // onConfirm(data.value);
}; // };
const [opened, setOpenStatus] = React.useState(focused); const [opened, setOpenStatus] = React.useState(focused);
const { change, data } = useForm({ value }, handleConfirm); const { change, data } = useForm(
{ value } /* commenting out temporarily handleConfirm */
);
const enable = () => setOpenStatus(true); const enable = () => setOpenStatus(true);
const disable = () => setOpenStatus(false); const disable = () => setOpenStatus(false);

View file

@ -0,0 +1,64 @@
import { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { exitFormPromptMessages as messages } from "./messages";
const useStyles = makeStyles(
() => ({
container: {
width: "100vw",
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center"
},
buttonsContainer: {
display: "flex",
justifyContent: "flex-end"
}
}),
{ name: "ExitFormPrompt" }
);
interface ExitFormDialogProps {
onSubmit: () => void;
onClose: () => void;
onLeave: () => void;
isOpen: boolean;
}
const ExitFormDialog: React.FC<ExitFormDialogProps> = ({
onSubmit,
onLeave,
onClose,
isOpen
}) => {
const classes = useStyles();
const intl = useIntl();
return (
<Dialog className={classes.container} open={isOpen}>
<CardTitle title={intl.formatMessage(messages.title)} onClose={onClose} />
<DialogContent>
<FormattedMessage {...messages.description} />
<CardSpacer />
<CardSpacer />
<div className={classes.buttonsContainer}>
<Button onClick={onLeave}>
{intl.formatMessage(messages.cancelButton)}
</Button>
<HorizontalSpacer />
<Button variant="contained" color="primary" onClick={onSubmit}>
{intl.formatMessage(messages.confirmButton)}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default ExitFormDialog;

View file

@ -0,0 +1,256 @@
import { SubmitPromise } from "@saleor/hooks/useForm";
import React, { useEffect, useRef, useState } from "react";
import { useHistory } from "react-router";
import useRouter from "use-react-router";
import ExitFormDialog from "./ExitFormDialog";
export interface ExitFormDialogData {
setIsDirty: (id: symbol, isDirty: boolean) => void;
setExitDialogSubmitRef: (id: symbol, submitFn: SubmitFn) => void;
setEnableExitDialog: (value: boolean) => void;
shouldBlockNavigation: () => boolean;
setIsSubmitting: (value: boolean) => void;
}
export type SubmitFn = (dataOrEvent?: any) => SubmitPromise<any[]>;
export type FormId = symbol;
type FormsData = Record<FormId, FormData>;
export interface WithFormId {
formId: FormId;
}
interface FormData {
isDirty: boolean;
submitFn: SubmitFn | null;
}
// Do not use this context directly in components
// use useExitFormDialog hook instead
export const ExitFormDialogContext = React.createContext<ExitFormDialogData>({
setIsDirty: () => undefined,
setEnableExitDialog: () => undefined,
setExitDialogSubmitRef: () => undefined,
shouldBlockNavigation: () => false,
setIsSubmitting: () => undefined
});
const defaultValues = {
isDirty: false,
showDialog: false,
blockNav: true,
navAction: null,
submit: null,
enableExitDialog: false,
isSubmitting: false,
formsData: {}
};
const ExitFormDialogProvider = ({ children }) => {
const history = useHistory();
const { history: routerHistory } = useRouter();
const [showDialog, setShowDialog] = useState(defaultValues.showDialog);
const isSubmitting = useRef(defaultValues.isSubmitting);
const formsData = useRef<FormsData>({});
const blockNav = useRef(defaultValues.blockNav);
const navAction = useRef(defaultValues.navAction);
const enableExitDialog = useRef(defaultValues.enableExitDialog);
const currentPath = useRef(window.location.pathname);
const setIsSubmitting = (value: boolean) => {
setEnableExitDialog(!value);
isSubmitting.current = value;
};
const setEnableExitDialog = (value: boolean) => {
// dialog should never be toggled to enabled during form submission
if (isSubmitting.current) {
return;
}
enableExitDialog.current = value;
};
const setDefaultFormsData = () => {
formsData.current = defaultValues.formsData;
};
const setCurrentPath = (newPath: string) => {
currentPath.current = newPath;
};
const setFormData = (id: symbol, newData: Partial<FormData>) => {
const updatedFormData = { ...formsData.current[id], ...newData };
formsData.current = {
...formsData.current,
[id]: updatedFormData
};
};
// Set either on generic form load or on every custom form data change
// but doesn't cause re-renders
const setSubmitRef = <T extends () => SubmitPromise<any[]>>(
id: symbol,
submitFn: T
) => {
setFormData(id, { submitFn });
};
const setIsDirty = (id: symbol, value: boolean) => {
// in case of race conitions between forms and transitions
if (!formsData.current[id]) {
return;
}
setFormData(id, { isDirty: value });
if (value) {
setEnableExitDialog(true);
}
};
const setBlockNav = (value: boolean) => (blockNav.current = value);
const setDefaultNavAction = () =>
(navAction.current = defaultValues.navAction);
const setStateDefaultValues = () => {
setIsSubmitting(defaultValues.isSubmitting);
setDefaultFormsData();
setShowDialog(defaultValues.showDialog);
setBlockNav(defaultValues.blockNav);
setEnableExitDialog(defaultValues.enableExitDialog);
setDefaultNavAction();
};
const getFormsDataValuesArray = () =>
Object.getOwnPropertySymbols(formsData.current).map(
key => formsData.current[key]
);
const hasAnyFormsDirty = () =>
getFormsDataValuesArray().some(({ isDirty }) => isDirty);
const shouldBlockNav = () => {
if (!enableExitDialog.current || !hasAnyFormsDirty()) {
return false;
}
return blockNav.current;
};
const isOnlyQuerying = transition =>
// wee need to compare to current path and not window location
// so it works with browser back button as well
transition.pathname === currentPath.current;
const handleNavigationBlock = () => {
const unblock = history.block(transition => {
// needs to be done before the shouldBlockNav condition
// so it doesnt trigger setting default values
if (isOnlyQuerying(transition)) {
// ransition type requires this function to return either
// false | void | string where string opens up the browser prompt
// hence we return null
return null;
}
if (shouldBlockNav()) {
navAction.current = transition;
setShowDialog(true);
return false;
}
setStateDefaultValues();
setCurrentPath(transition.pathname);
return null;
});
return unblock;
};
useEffect(handleNavigationBlock, []);
const continueNavigation = () => {
setBlockNav(false);
setDefaultFormsData();
setCurrentPath(navAction.current.pathname);
// because our useNavigator navigate action may be blocked
// by exit dialog we want to avoid using it doing this transition
routerHistory.push(navAction.current.pathname);
setStateDefaultValues();
};
const getDirtyFormsSubmitFn = () =>
getFormsDataValuesArray()
.filter(({ isDirty }) => isDirty)
.map(({ submitFn }) => submitFn);
const hasAnySubmitFn = () =>
getFormsDataValuesArray().some(({ submitFn }) => !!submitFn);
const handleSubmit = async () => {
if (!hasAnySubmitFn()) {
return;
}
setShowDialog(false);
setIsSubmitting(true);
const errors = await Promise.all(
getDirtyFormsSubmitFn().map(submitFn => submitFn())
);
const isError = errors.flat().some(errors => errors);
setIsSubmitting(false);
if (!isError) {
continueNavigation();
}
setDefaultNavAction();
};
const handleLeave = () => {
continueNavigation();
};
const handleClose = () => {
setDefaultNavAction();
setShowDialog(false);
};
// Used to prevent race conditions from places such as
// create pages with navigation on mutation completed
const shouldBlockNavigation = () => !!navAction.current;
const providerData: ExitFormDialogData = {
setIsDirty,
shouldBlockNavigation,
setEnableExitDialog,
setExitDialogSubmitRef: setSubmitRef,
setIsSubmitting
};
return (
<ExitFormDialogContext.Provider value={providerData}>
<ExitFormDialog
isOpen={showDialog}
onSubmit={handleSubmit}
onLeave={handleLeave}
onClose={handleClose}
/>
{children}
</ExitFormDialogContext.Provider>
);
};
export default ExitFormDialogProvider;

View file

@ -1,18 +1,28 @@
import useForm, { SubmitPromise, UseFormResult } from "@saleor/hooks/useForm"; import useForm, { SubmitPromise, UseFormResult } from "@saleor/hooks/useForm";
import React from "react"; import React from "react";
export interface FormProps<T> import { FormId } from "./ExitFormDialogProvider";
export interface FormProps<TData, TErrors>
extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit"> { extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit"> {
children: (props: UseFormResult<T>) => React.ReactNode; children: (props: UseFormResult<TData>) => React.ReactNode;
confirmLeave?: boolean; confirmLeave?: boolean;
initial?: T; initial?: TData;
resetOnSubmit?: boolean; resetOnSubmit?: boolean;
onSubmit?: (data: T) => SubmitPromise | void; onSubmit?: (data: TData) => SubmitPromise<TErrors[]> | void;
formId?: FormId;
} }
function Form<T>(props: FormProps<T>) { function Form<TData, Terrors>({
const { children, initial, resetOnSubmit, onSubmit, ...rest } = props; children,
const renderProps = useForm(initial, onSubmit); initial,
resetOnSubmit,
onSubmit,
confirmLeave = false,
formId,
...rest
}: FormProps<TData, Terrors>) {
const renderProps = useForm(initial, onSubmit, { confirmLeave, formId });
function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) { function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) {
const { reset, submit } = renderProps; const { reset, submit } = renderProps;

View file

@ -0,0 +1,21 @@
import { defineMessages } from "react-intl";
export const exitFormPromptMessages = defineMessages({
title: {
defaultMessage: "Are you sure you want to leave?",
description: "ExitFormPrompt title"
},
description: {
defaultMessage:
"You have unsaved changes on this view. What would you like to do with them?",
description: "ExitFormPrompt description"
},
cancelButton: {
defaultMessage: "leave without saving",
description: "ExitFormPrompt cancel button"
},
confirmButton: {
defaultMessage: "save & continue",
description: "ExitFormPrompt confirm button"
}
});

View file

@ -0,0 +1,40 @@
import { useContext, useRef } from "react";
import {
ExitFormDialogContext,
ExitFormDialogData,
SubmitFn,
WithFormId
} from "./ExitFormDialogProvider";
export interface UseExitFormDialogResult
extends Pick<
ExitFormDialogData,
"setEnableExitDialog" | "shouldBlockNavigation" | "setIsSubmitting"
>,
WithFormId {
setIsDirty: (isDirty: boolean) => void;
setExitDialogSubmitRef: (submitFn: SubmitFn) => void;
}
export interface UseExitFormDialogProps {
formId: symbol;
}
export const useExitFormDialog = (
{ formId }: UseExitFormDialogProps = { formId: undefined }
): UseExitFormDialogResult => {
const id = useRef(formId || Symbol()).current;
const { setIsDirty, setExitDialogSubmitRef, ...rest } = useContext(
ExitFormDialogContext
);
return {
...rest,
formId: id,
setIsDirty: (value: boolean) => setIsDirty(id, value),
setExitDialogSubmitRef: (submitFn: SubmitFn) =>
setExitDialogSubmitRef(id, submitFn)
};
};

View file

@ -1,23 +0,0 @@
import { Tooltip } from "@material-ui/core";
import React from "react";
interface ButtonTooltipDecoratorProps {
tooltip?: string;
}
export const ButtonTooltipDecorator: React.FC<ButtonTooltipDecoratorProps> = ({
tooltip,
children
}) => {
if (tooltip) {
return (
<Tooltip title={tooltip} placement="top">
<span>{children}</span>
</Tooltip>
);
}
return <>{children}</>;
};
ButtonTooltipDecorator.displayName = "ButtonTooltipDecorator";
export default ButtonTooltipDecorator;

View file

@ -6,9 +6,11 @@ import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment"; import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment";
import useAddressValidation from "@saleor/hooks/useAddressValidation"; import useAddressValidation from "@saleor/hooks/useAddressValidation";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
import { extractMutationErrors } from "@saleor/misc";
import { AddressInput } from "@saleor/types/globalTypes"; import { AddressInput } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { mapCountriesToChoices } from "@saleor/utils/maps"; import { mapCountriesToChoices } from "@saleor/utils/maps";
@ -56,7 +58,7 @@ export interface CustomerCreatePageProps {
errors: AccountErrorFragment[]; errors: AccountErrorFragment[];
saveButtonBar: ConfirmButtonTransitionState; saveButtonBar: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit: (data: CustomerCreatePageSubmitData) => void; onSubmit: (data: CustomerCreatePageSubmitData) => SubmitPromise;
} }
const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
@ -117,20 +119,22 @@ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
.some(field => field !== ""); .some(field => field !== "");
if (areAddressInputFieldsModified) { if (areAddressInputFieldsModified) {
handleSubmitWithAddress(formData); return handleSubmitWithAddress(formData);
} else { }
return extractMutationErrors(
onSubmit({ onSubmit({
address: null, address: null,
customerFirstName: formData.customerFirstName, customerFirstName: formData.customerFirstName,
customerLastName: formData.customerLastName, customerLastName: formData.customerLastName,
email: formData.email, email: formData.email,
note: formData.note note: formData.note
}); })
} );
}; };
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit} confirmLeave> <Form confirmLeave initial={initialForm} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
const handleCountrySelect = createSingleAutocompleteSelectHandler( const handleCountrySelect = createSingleAutocompleteSelectHandler(
change, change,

View file

@ -6,6 +6,7 @@ import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { UpdateCustomer_customerUpdate_errors } from "@saleor/customers/types/UpdateCustomer";
import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment"; import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment";
import CustomerGiftCardsCard from "@saleor/giftCards/components/GiftCardCustomerCard/CustomerGiftCardsCard"; import CustomerGiftCardsCard from "@saleor/giftCards/components/GiftCardCustomerCard/CustomerGiftCardsCard";
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
@ -39,7 +40,9 @@ export interface CustomerDetailsPageProps {
errors: AccountErrorFragment[]; errors: AccountErrorFragment[];
saveButtonBar: ConfirmButtonTransitionState; saveButtonBar: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit: (data: CustomerDetailsPageFormData) => SubmitPromise; onSubmit: (
data: CustomerDetailsPageFormData
) => SubmitPromise<UpdateCustomer_customerUpdate_errors[]>;
onViewAllOrdersClick: () => void; onViewAllOrdersClick: () => void;
onRowClick: (id: string) => void; onRowClick: (id: string) => void;
onAddressManageClick: () => void; onAddressManageClick: () => void;
@ -75,7 +78,7 @@ const CustomerDetailsPage: React.FC<CustomerDetailsPageProps> = ({
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);

View file

@ -4,7 +4,7 @@ import useNotifier from "@saleor/hooks/useNotifier";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { maybe } from "../../misc"; import { extractMutationErrors, maybe } from "../../misc";
import CustomerCreatePage from "../components/CustomerCreatePage"; import CustomerCreatePage from "../components/CustomerCreatePage";
import { TypedCreateCustomerMutation } from "../mutations"; import { TypedCreateCustomerMutation } from "../mutations";
import { TypedCustomerCreateDataQuery } from "../queries"; import { TypedCustomerCreateDataQuery } from "../queries";
@ -27,11 +27,29 @@ export const CustomerCreate: React.FC<{}> = () => {
navigate(customerUrl(data.customerCreate.user.id)); navigate(customerUrl(data.customerCreate.user.id));
} }
}; };
return ( return (
<TypedCustomerCreateDataQuery displayLoader> <TypedCustomerCreateDataQuery displayLoader>
{({ data, loading }) => ( {({ data, loading }) => (
<TypedCreateCustomerMutation onCompleted={handleCreateCustomerSuccess}> <TypedCreateCustomerMutation onCompleted={handleCreateCustomerSuccess}>
{(createCustomer, createCustomerOpts) => ( {(createCustomer, createCustomerOpts) => {
const handleSubmit = formData =>
extractMutationErrors(
createCustomer({
variables: {
input: {
defaultBillingAddress: formData.address,
defaultShippingAddress: formData.address,
email: formData.email,
firstName: formData.customerFirstName,
lastName: formData.customerLastName,
note: formData.note
}
}
})
);
return (
<> <>
<WindowTitle <WindowTitle
title={intl.formatMessage({ title={intl.formatMessage({
@ -45,23 +63,11 @@ export const CustomerCreate: React.FC<{}> = () => {
errors={createCustomerOpts.data?.customerCreate.errors || []} errors={createCustomerOpts.data?.customerCreate.errors || []}
saveButtonBar={createCustomerOpts.status} saveButtonBar={createCustomerOpts.status}
onBack={() => navigate(customerListUrl())} onBack={() => navigate(customerListUrl())}
onSubmit={formData => { onSubmit={handleSubmit}
createCustomer({
variables: {
input: {
defaultBillingAddress: formData.address,
defaultShippingAddress: formData.address,
email: formData.email,
firstName: formData.customerFirstName,
lastName: formData.customerLastName,
note: formData.note
}
}
});
}}
/> />
</> </>
)} );
}}
</TypedCreateCustomerMutation> </TypedCreateCustomerMutation>
)} )}
</TypedCustomerCreateDataQuery> </TypedCustomerCreateDataQuery>

View file

@ -3,7 +3,7 @@ import ActionDialog from "@saleor/components/ActionDialog";
import NotFoundPage from "@saleor/components/NotFoundPage"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { getStringOrPlaceholder } from "@saleor/misc"; import { extractMutationErrors, getStringOrPlaceholder } from "@saleor/misc";
import { MutationResultAdditionalProps } from "@saleor/types"; import { MutationResultAdditionalProps } from "@saleor/types";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { import {
@ -69,8 +69,9 @@ export const CustomerDetailsContent: React.FC<CustomerDetailsContentProps> = ({
const [updateMetadata] = useMetadataUpdate({}); const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const updateData = async (data: CustomerDetailsPageFormData) => { const updateData = async (data: CustomerDetailsPageFormData) =>
const result = await updateCustomer({ extractMutationErrors(
updateCustomer({
variables: { variables: {
id, id,
input: { input: {
@ -81,10 +82,8 @@ export const CustomerDetailsContent: React.FC<CustomerDetailsContentProps> = ({
note: data.note note: data.note
} }
} }
}); })
);
return result.data.customerUpdate.errors;
};
const handleSubmit = createMetadataUpdateHandler( const handleSubmit = createMetadataUpdateHandler(
user, user,

View file

@ -17,6 +17,7 @@ import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle"; import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { filter } from "fuzzaldrin"; import { filter } from "fuzzaldrin";
@ -37,7 +38,7 @@ export interface DiscountCountrySelectDialogProps {
initial: string[]; initial: string[];
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onConfirm: (data: FormData) => void; onConfirm: (data: FormData) => SubmitPromise;
} }
const DiscountCountrySelectDialog: React.FC<DiscountCountrySelectDialogProps> = props => { const DiscountCountrySelectDialog: React.FC<DiscountCountrySelectDialogProps> = props => {
@ -90,7 +91,9 @@ const DiscountCountrySelectDialog: React.FC<DiscountCountrySelectDialogProps> =
<TextField <TextField
name="query" name="query"
value={data.query} value={data.query}
onChange={event => change(event, () => fetch(data.query))} onChange={event =>
change(event /* TO BE CHECKED: () => fetch(data.query)*/)
}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Filter Countries", defaultMessage: "Filter Countries",
description: "search box label" description: "search box label"

View file

@ -8,7 +8,9 @@ import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { createSaleChannelsChangeHandler } from "@saleor/discounts/handlers"; import { createSaleChannelsChangeHandler } from "@saleor/discounts/handlers";
import { SALE_CREATE_FORM_ID } from "@saleor/discounts/views/SaleCreate/consts";
import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment"; import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -47,7 +49,7 @@ export interface SaleCreatePageProps {
onBack: () => void; onBack: () => void;
onChannelsChange: (data: ChannelSaleFormData[]) => void; onChannelsChange: (data: ChannelSaleFormData[]) => void;
openChannelsModal: () => void; openChannelsModal: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: FormData) => SubmitPromise<any[]>;
} }
const SaleCreatePage: React.FC<SaleCreatePageProps> = ({ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
@ -79,8 +81,14 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
metadata: [], metadata: [],
privateMetadata: [] privateMetadata: []
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit}> <Form
confirmLeave
initial={initialForm}
onSubmit={onSubmit}
formId={SALE_CREATE_FORM_ID}
>
{({ change, data, hasChanged, submit, triggerChange }) => { {({ change, data, hasChanged, submit, triggerChange }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,

View file

@ -9,7 +9,9 @@ import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { Tab, TabContainer } from "@saleor/components/Tab"; import { Tab, TabContainer } from "@saleor/components/Tab";
import { createSaleChannelsChangeHandler } from "@saleor/discounts/handlers"; import { createSaleChannelsChangeHandler } from "@saleor/discounts/handlers";
import { SALE_UPDATE_FORM_ID } from "@saleor/discounts/views/SaleDetails/types";
import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment"; import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -88,7 +90,7 @@ export interface SaleDetailsPageProps
onVariantUnassign: (id: string) => void; onVariantUnassign: (id: string) => void;
onVariantClick: (productId: string, variantId: string) => () => void; onVariantClick: (productId: string, variantId: string) => () => void;
onRemove: () => void; onRemove: () => void;
onSubmit: (data: SaleDetailsPageFormData) => void; onSubmit: (data: SaleDetailsPageFormData) => SubmitPromise<any[]>;
onTabClick: (index: SaleDetailsPageTab) => void; onTabClick: (index: SaleDetailsPageTab) => void;
onChannelsChange: (data: ChannelSaleFormData[]) => void; onChannelsChange: (data: ChannelSaleFormData[]) => void;
openChannelsModal: () => void; openChannelsModal: () => void;
@ -157,7 +159,12 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput) privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput)
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit}> <Form
confirmLeave
initial={initialForm}
onSubmit={onSubmit}
formId={SALE_UPDATE_FORM_ID}
>
{({ change, data, hasChanged, submit, triggerChange }) => { {({ change, data, hasChanged, submit, triggerChange }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,

View file

@ -4,14 +4,16 @@ import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCar
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { import {
createChannelsChangeHandler, createChannelsChangeHandler,
createDiscountTypeChangeHandler createDiscountTypeChangeHandler
} from "@saleor/discounts/handlers"; } from "@saleor/discounts/handlers";
import { VOUCHER_CREATE_FORM_ID } from "@saleor/discounts/views/VoucherCreate/types";
import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment"; import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -23,30 +25,14 @@ import { useIntl } from "react-intl";
import { PermissionEnum, VoucherTypeEnum } from "../../../types/globalTypes"; import { PermissionEnum, VoucherTypeEnum } from "../../../types/globalTypes";
import { DiscountTypeEnum, RequirementsPicker } from "../../types"; import { DiscountTypeEnum, RequirementsPicker } from "../../types";
import VoucherDates from "../VoucherDates"; import VoucherDates from "../VoucherDates";
import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
import VoucherInfo from "../VoucherInfo"; import VoucherInfo from "../VoucherInfo";
import VoucherLimits from "../VoucherLimits"; import VoucherLimits from "../VoucherLimits";
import VoucherRequirements from "../VoucherRequirements"; import VoucherRequirements from "../VoucherRequirements";
import VoucherTypes from "../VoucherTypes"; import VoucherTypes from "../VoucherTypes";
import VoucherValue from "../VoucherValue"; import VoucherValue from "../VoucherValue";
export interface FormData extends MetadataFormData { export interface FormData extends VoucherDetailsPageFormData {
applyOncePerCustomer: boolean;
applyOncePerOrder: boolean;
onlyForStaff: boolean;
channelListings: ChannelVoucherData[];
code: string;
discountType: DiscountTypeEnum;
endDate: string;
endTime: string;
hasEndDate: boolean;
hasUsageLimit: boolean;
minCheckoutItemsQuantity: string;
requirementsPicker: RequirementsPicker;
startDate: string;
startTime: string;
type: VoucherTypeEnum;
usageLimit: number;
used: number;
value: number; value: number;
} }
@ -60,7 +46,7 @@ export interface VoucherCreatePageProps {
onBack: () => void; onBack: () => void;
onChannelsChange: (data: ChannelVoucherData[]) => void; onChannelsChange: (data: ChannelVoucherData[]) => void;
openChannelsModal: () => void; openChannelsModal: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: FormData) => SubmitPromise;
} }
const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
@ -104,7 +90,12 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit}> <Form
confirmLeave
initial={initialForm}
onSubmit={onSubmit}
formId={VOUCHER_CREATE_FORM_ID}
>
{({ change, data, hasChanged, submit, triggerChange, set }) => { {({ change, data, hasChanged, submit, triggerChange, set }) => {
const handleDiscountTypeChange = createDiscountTypeChangeHandler( const handleDiscountTypeChange = createDiscountTypeChangeHandler(
change change

View file

@ -192,7 +192,7 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit}> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit, triggerChange, set }) => { {({ change, data, hasChanged, submit, triggerChange, set }) => {
const handleDiscountTypeChange = createDiscountTypeChangeHandler( const handleDiscountTypeChange = createDiscountTypeChangeHandler(
change change

View file

@ -29,6 +29,7 @@ import {
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { SALE_CREATE_FORM_ID } from "./consts";
import { createHandler } from "./handlers"; import { createHandler } from "./handlers";
interface SaleCreateProps { interface SaleCreateProps {
@ -63,7 +64,12 @@ export const SaleCreateView: React.FC<SaleCreateProps> = ({ params }) => {
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(allChannels, params?.action, { closeModal, openModal }); } = useChannels(
allChannels,
params?.action,
{ closeModal, openModal },
{ formId: SALE_CREATE_FORM_ID }
);
const [updateChannels, updateChannelsOpts] = useSaleChannelListingUpdate({}); const [updateChannels, updateChannelsOpts] = useSaleChannelListingUpdate({});

View file

@ -0,0 +1 @@
export const SALE_CREATE_FORM_ID = Symbol();

View file

@ -8,7 +8,11 @@ import {
SaleCreate, SaleCreate,
SaleCreateVariables SaleCreateVariables
} from "@saleor/discounts/types/SaleCreate"; } from "@saleor/discounts/types/SaleCreate";
import { joinDateTime } from "@saleor/misc"; import {
extractMutationErrors,
getMutationErrors,
joinDateTime
} from "@saleor/misc";
import { decimal } from "@saleor/misc"; import { decimal } from "@saleor/misc";
import { DiscountValueTypeEnum, SaleType } from "@saleor/types/globalTypes"; import { DiscountValueTypeEnum, SaleType } from "@saleor/types/globalTypes";
import { MutationFetchResult } from "react-apollo"; import { MutationFetchResult } from "react-apollo";
@ -40,14 +44,25 @@ export function createHandler(
} }
}); });
if (!response.data.saleCreate.errors.length) { const errors = getMutationErrors(response);
if (errors.length > 0) {
return { errors };
}
const updateChannelsErrors = await extractMutationErrors(
updateChannels({ updateChannels({
variables: getSaleChannelsVariables( variables: getSaleChannelsVariables(
response.data.saleCreate.sale.id, response.data.saleCreate.sale.id,
formData formData
) )
}); })
return response.data.saleCreate.sale.id; );
if (updateChannelsErrors.length > 0) {
return { errors: updateChannelsErrors };
} }
return { id: response.data.saleCreate.sale.id };
}; };
} }

View file

@ -62,6 +62,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { createUpdateHandler } from "./handlers"; import { createUpdateHandler } from "./handlers";
import { messages } from "./messages"; import { messages } from "./messages";
import { SALE_UPDATE_FORM_ID } from "./types";
interface SaleDetailsProps { interface SaleDetailsProps {
id: string; id: string;
@ -144,10 +145,15 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(saleChannelsChoices, params?.action, { } = useChannels(
saleChannelsChoices,
params?.action,
{
closeModal, closeModal,
openModal openModal
}); },
{ formId: SALE_UPDATE_FORM_ID }
);
const [selectedChannel] = useLocalStorage("salesListChannel", ""); const [selectedChannel] = useLocalStorage("salesListChannel", "");

View file

@ -0,0 +1 @@
export const SALE_UPDATE_FORM_ID = Symbol();

View file

@ -32,6 +32,7 @@ import {
voucherUrl voucherUrl
} from "../../urls"; } from "../../urls";
import { createHandler } from "./handlers"; import { createHandler } from "./handlers";
import { VOUCHER_CREATE_FORM_ID } from "./types";
interface VoucherCreateProps { interface VoucherCreateProps {
params: VoucherCreateUrlQueryParams; params: VoucherCreateUrlQueryParams;
@ -65,7 +66,12 @@ export const VoucherCreateView: React.FC<VoucherCreateProps> = ({ params }) => {
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(allChannels, params?.action, { closeModal, openModal }); } = useChannels(
allChannels,
params?.action,
{ closeModal, openModal },
{ formId: VOUCHER_CREATE_FORM_ID }
);
const [updateChannels, updateChannelsOpts] = useVoucherChannelListingUpdate( const [updateChannels, updateChannelsOpts] = useVoucherChannelListingUpdate(
{} {}

View file

@ -9,7 +9,11 @@ import {
VoucherCreate, VoucherCreate,
VoucherCreateVariables VoucherCreateVariables
} from "@saleor/discounts/types/VoucherCreate"; } from "@saleor/discounts/types/VoucherCreate";
import { joinDateTime } from "@saleor/misc"; import {
extractMutationErrors,
getMutationErrors,
joinDateTime
} from "@saleor/misc";
import { import {
DiscountValueTypeEnum, DiscountValueTypeEnum,
VoucherTypeEnum VoucherTypeEnum
@ -53,15 +57,26 @@ export function createHandler(
} }
}); });
if (!response.data.voucherCreate.errors.length) { const errors = getMutationErrors(response);
if (errors.length > 0) {
return { errors };
}
const channelsUpdateErrors = await extractMutationErrors(
updateChannels({ updateChannels({
variables: getChannelsVariables( variables: getChannelsVariables(
response.data.voucherCreate.voucher.id, response.data.voucherCreate.voucher.id,
formData, formData,
formData.channelListings formData.channelListings
) )
}); })
return response.data.voucherCreate.voucher.id; );
if (channelsUpdateErrors.length > 0) {
return { errors: channelsUpdateErrors };
} }
return { id: response.data.voucherCreate.voucher.id };
}; };
} }

View file

@ -0,0 +1 @@
export const VOUCHER_CREATE_FORM_ID = Symbol();

View file

@ -63,6 +63,7 @@ import { collectionUrl } from "../../../collections/urls";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { productUrl } from "../../../products/urls"; import { productUrl } from "../../../products/urls";
import { createUpdateHandler } from "./handlers"; import { createUpdateHandler } from "./handlers";
import { VOUCHER_UPDATE_FORM_ID } from "./types";
interface VoucherDetailsProps { interface VoucherDetailsProps {
id: string; id: string;
@ -152,10 +153,15 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
isChannelsModalOpen, isChannelsModalOpen,
setCurrentChannels, setCurrentChannels,
toggleAllChannels toggleAllChannels
} = useChannels(voucherChannelsChoices, params?.action, { } = useChannels(
voucherChannelsChoices,
params?.action,
{
closeModal, closeModal,
openModal openModal
}); },
{ formId: VOUCHER_UPDATE_FORM_ID }
);
const [updateChannels, updateChannelsOpts] = useVoucherChannelListingUpdate( const [updateChannels, updateChannelsOpts] = useVoucherChannelListingUpdate(
{} {}

View file

@ -0,0 +1 @@
export const VOUCHER_UPDATE_FORM_ID = Symbol();

View file

@ -3,10 +3,10 @@ import { GiftCardError } from "@saleor/fragments/types/GiftCardError";
import { giftCardUpdateFormMessages } from "@saleor/giftCards/GiftCardsList/messages"; import { giftCardUpdateFormMessages } from "@saleor/giftCards/GiftCardsList/messages";
import { MutationResultWithOpts } from "@saleor/hooks/makeMutation"; import { MutationResultWithOpts } from "@saleor/hooks/makeMutation";
import useForm, { FormChange, UseFormResult } from "@saleor/hooks/useForm"; import useForm, { FormChange, UseFormResult } from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils"; import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils";
import { getFormErrors } from "@saleor/utils/errors"; import { getFormErrors } from "@saleor/utils/errors";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
@ -131,9 +131,22 @@ const GiftCardUpdateFormProvider: React.FC<GiftCardUpdateFormProviderProps> = ({
return result?.data?.giftCardUpdate?.errors; return result?.data?.giftCardUpdate?.errors;
}; };
const formProps = useForm<GiftCardUpdateFormData>(getInitialData()); const formProps = useForm(getInitialData());
const { data, change, setChanged, hasChanged } = formProps; const { data, change, setChanged, hasChanged, formId } = formProps;
const handleSubmit = createMetadataUpdateHandler(
giftCard,
submit,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit: handleSubmit,
setChanged
});
const { const {
isMetadataModified, isMetadataModified,
@ -148,15 +161,7 @@ const GiftCardUpdateFormProvider: React.FC<GiftCardUpdateFormProviderProps> = ({
...getMetadata(data, isMetadataModified, isPrivateMetadataModified) ...getMetadata(data, isMetadataModified, isPrivateMetadataModified)
}; };
const handleSubmit = createMetadataUpdateHandler( const formSubmit = () => handleFormSubmit(submitData);
giftCard,
submit,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const formSubmit = () =>
handleFormSubmit(submitData, handleSubmit, setChanged);
const formErrors = getFormErrors( const formErrors = getFormErrors(
["tags", "expiryDate"], ["tags", "expiryDate"],

View file

@ -11,7 +11,9 @@ import { useState } from "react";
interface UseAddressValidation<TInput, TOutput> { interface UseAddressValidation<TInput, TOutput> {
errors: AccountErrorFragment[]; errors: AccountErrorFragment[];
submit: (data: TInput & AddressTypeInput) => TOutput; submit: (
data: TInput & AddressTypeInput
) => TOutput | Promise<AccountErrorFragment[]>;
} }
function useAddressValidation<TInput, TOutput>( function useAddressValidation<TInput, TOutput>(
@ -42,7 +44,10 @@ function useAddressValidation<TInput, TOutput>(
); );
return onSubmit(transformFormToAddressInput(data)); return onSubmit(transformFormToAddressInput(data));
} catch { } catch {
setValidationErrors(add(countryRequiredError, validationErrors)); const errors = add(countryRequiredError, validationErrors);
setValidationErrors(errors);
// since every onSubmit must return Promise<error>
return Promise.resolve(errors);
} }
} }
}; };

View file

@ -20,10 +20,15 @@ describe("useChannels", () => {
it("properly toggles channels", () => { it("properly toggles channels", () => {
// Given // Given
const { result } = renderHook(() => const { result } = renderHook(() =>
useChannels(channels, "", { useChannels(
channels,
"",
{
closeModal: jest.fn, closeModal: jest.fn,
openModal: jest.fn openModal: jest.fn
}) },
{ formId: Symbol() }
)
); );
// When // When
@ -42,10 +47,15 @@ describe("useChannels", () => {
it("properly removes channels", () => { it("properly removes channels", () => {
// Given // Given
const { result } = renderHook(() => const { result } = renderHook(() =>
useChannels(channels, "", { useChannels(
channels,
"",
{
closeModal: jest.fn, closeModal: jest.fn,
openModal: jest.fn openModal: jest.fn
}) },
{ formId: Symbol() }
)
); );
// When // When
@ -68,10 +78,15 @@ describe("useChannels", () => {
it("doesn't not save changes if closed without confirm", () => { it("doesn't not save changes if closed without confirm", () => {
// Given // Given
const { result } = renderHook(() => const { result } = renderHook(() =>
useChannels(channels, "", { useChannels(
channels,
"",
{
closeModal: jest.fn, closeModal: jest.fn,
openModal: jest.fn openModal: jest.fn
}) },
{ formId: Symbol() }
)
); );
// When // When

View file

@ -1,5 +1,7 @@
import { ChannelsAction } from "@saleor/channels/urls"; import { ChannelsAction } from "@saleor/channels/urls";
import { Channel } from "@saleor/channels/utils"; import { Channel } from "@saleor/channels/utils";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import useListActions from "@saleor/hooks/useListActions"; import useListActions from "@saleor/hooks/useListActions";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -11,8 +13,15 @@ interface Modal {
function useChannels<T extends Channel, A>( function useChannels<T extends Channel, A>(
channels: T[], channels: T[],
action: A | ChannelsAction, action: A | ChannelsAction,
{ closeModal, openModal }: Modal { closeModal, openModal }: Modal,
opts: WithFormId
) { ) {
const { formId } = opts;
const { setIsDirty } = useExitFormDialog({
formId
});
const [currentChannels, setCurrentChannels] = useStateFromProps(channels); const [currentChannels, setCurrentChannels] = useStateFromProps(channels);
const { const {
@ -34,6 +43,11 @@ function useChannels<T extends Channel, A>(
(channel, nextChannel) => channel.name.localeCompare(nextChannel.name) (channel, nextChannel) => channel.name.localeCompare(nextChannel.name)
); );
setCurrentChannels(sortedChannelListElements); setCurrentChannels(sortedChannelListElements);
// hack so channels also update exit form dalog provider
// despite not setting page's form data "changed" prop
setIsDirty(true);
closeModal(); closeModal();
}; };

View file

@ -1,7 +1,13 @@
import { FormId } from "@saleor/components/Form/ExitFormDialogProvider";
import {
useExitFormDialog,
UseExitFormDialogResult
} from "@saleor/components/Form/useExitFormDialog";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { toggle } from "@saleor/utils/lists"; import { toggle } from "@saleor/utils/lists";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import omit from "lodash/omit"; import omit from "lodash/omit";
import React from "react"; import React, { useEffect } from "react";
import { useState } from "react"; import { useState } from "react";
import useStateFromProps from "./useStateFromProps"; import useStateFromProps from "./useStateFromProps";
@ -12,7 +18,7 @@ export interface ChangeEvent<TData = any> {
value: TData; value: TData;
}; };
} }
export type SubmitPromise = Promise<any[]>; export type SubmitPromise<TData = any> = Promise<TData>;
export type FormChange = (event: ChangeEvent, cb?: () => void) => void; export type FormChange = (event: ChangeEvent, cb?: () => void) => void;
@ -20,19 +26,35 @@ export type FormErrors<T> = {
[field in keyof T]?: string | React.ReactNode; [field in keyof T]?: string | React.ReactNode;
}; };
export interface UseFormResult<T> { export interface UseFormOpts {
change: FormChange; confirmLeave: boolean;
data: T; formId?: FormId;
hasChanged: boolean; }
export interface UseFormResult<TData>
extends CommonUseFormResult<TData>,
Pick<UseExitFormDialogResult, "formId"> {
reset: () => void; reset: () => void;
set: (data: Partial<T>) => void; set: (data: Partial<TData>) => void;
submit: () => void;
triggerChange: () => void; triggerChange: () => void;
toggleValue: FormChange;
errors: FormErrors<T>;
setChanged: (value: boolean) => void; setChanged: (value: boolean) => void;
setError: (name: keyof T, error: string | React.ReactNode) => void; handleChange: FormChange;
clearErrors: (name?: keyof T | Array<keyof T>) => void; toggleValue: FormChange;
errors: FormErrors<TData>;
setError: (name: keyof TData, error: string | React.ReactNode) => void;
clearErrors: (name?: keyof TData | Array<keyof TData>) => void;
}
export interface CommonUseFormResult<TData> {
data: TData;
change: FormChange;
hasChanged: boolean;
submit: (dataOrEvent?: any) => SubmitPromise<any[]>;
}
export interface CommonUseFormResultWithHandlers<TData, THandlers>
extends CommonUseFormResult<TData> {
handlers: THandlers;
} }
type FormData = Record<string, any | any[]>; type FormData = Record<string, any | any[]>;
@ -60,25 +82,61 @@ function handleRefresh<T extends FormData>(
} }
} }
function useForm<T extends FormData>( function useForm<T extends FormData, TErrors>(
initial: T, initialData: T,
onSubmit?: (data: T) => SubmitPromise | void onSubmit?: (data: T) => SubmitPromise<TErrors[]> | void,
opts: UseFormOpts = { confirmLeave: false, formId: undefined }
): UseFormResult<T> { ): UseFormResult<T> {
const { confirmLeave, formId: propsFormId } = opts;
const [hasChanged, setChanged] = useState(false); const [hasChanged, setChanged] = useState(false);
const [errors, setErrors] = useState<FormErrors<T>>({}); const [errors, setErrors] = useState<FormErrors<T>>({});
const [data, setData] = useStateFromProps(initial, { const [data, setData] = useStateFromProps(initialData, {
mergeFunc: merge, mergeFunc: merge,
onRefresh: newData => handleRefresh(data, newData, setChanged) onRefresh: newData => handleRefresh(data, newData, handleSetChanged)
}); });
const {
setIsDirty: setIsFormDirtyInExitDialog,
setExitDialogSubmitRef,
setEnableExitDialog,
formId
} = useExitFormDialog({ formId: propsFormId });
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const handleSetChanged = (value: boolean = true) => {
setChanged(value);
if (confirmLeave) {
setIsFormDirtyInExitDialog(value);
}
};
const setExitDialogData = () => {
setEnableExitDialog(true);
if (!onSubmit) {
return;
}
setExitDialogSubmitRef(submit);
};
useEffect(setExitDialogData, [onSubmit]);
function toggleValue(event: ChangeEvent, cb?: () => void) { function toggleValue(event: ChangeEvent, cb?: () => void) {
const { name, value } = event.target; const { name, value } = event.target;
const field = data[name as keyof T]; const field = data[name as keyof T];
if (Array.isArray(field)) { if (Array.isArray(field)) {
if (!hasChanged) { if (!hasChanged) {
setChanged(true); handleSetChanged(true);
} }
setData({ setData({
...data, ...data,
[name]: toggle(value, field, isEqual) [name]: toggle(value, field, isEqual)
@ -90,6 +148,11 @@ function useForm<T extends FormData>(
} }
} }
const handleChange: FormChange = event => {
change(event);
handleSetChanged(true);
};
function change(event: ChangeEvent) { function change(event: ChangeEvent) {
const { name, value } = event.target; const { name, value } = event.target;
@ -98,7 +161,7 @@ function useForm<T extends FormData>(
return; return;
} else { } else {
if (data[name] !== value) { if (data[name] !== value) {
setChanged(true); handleSetChanged(true);
} }
setData(data => ({ setData(data => ({
...data, ...data,
@ -108,7 +171,7 @@ function useForm<T extends FormData>(
} }
function reset() { function reset() {
setData(initial); setData(initialData);
} }
function set(newData: Partial<T>, setHasChanged = true) { function set(newData: Partial<T>, setHasChanged = true) {
@ -116,23 +179,15 @@ function useForm<T extends FormData>(
...data, ...data,
...newData ...newData
})); }));
setChanged(setHasChanged); handleSetChanged(setHasChanged);
} }
async function submit() { async function submit() {
if (typeof onSubmit === "function" && !Object.keys(errors).length) { if (typeof onSubmit === "function" && !Object.keys(errors).length) {
const result = onSubmit(data); const result = handleFormSubmit(data);
if (result) {
const errors = await result;
if (errors?.length === 0) {
setChanged(false);
}
}
}
}
function triggerChange() { return result;
setChanged(true); }
} }
const setError = (field: keyof T, error: string | React.ReactNode) => const setError = (field: keyof T, error: string | React.ReactNode) =>
@ -149,6 +204,7 @@ function useForm<T extends FormData>(
}; };
return { return {
formId,
setError, setError,
errors, errors,
change, change,
@ -156,11 +212,12 @@ function useForm<T extends FormData>(
data, data,
hasChanged, hasChanged,
reset, reset,
setChanged,
set, set,
submit, submit,
toggleValue, toggleValue,
triggerChange handleChange,
triggerChange: handleSetChanged,
setChanged: handleSetChanged
}; };
} }

View file

@ -0,0 +1,45 @@
import { FormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { SubmitPromise } from "@saleor/hooks/useForm";
interface UseHandleFormSubmitProps<TData, TErrors> {
formId?: FormId;
onSubmit: (data: TData) => SubmitPromise<TErrors[]> | void;
setChanged: (changed: boolean) => void;
}
function useHandleFormSubmit<TData, TErrors>({
formId,
onSubmit,
setChanged
}: UseHandleFormSubmitProps<TData, TErrors>) {
const { setIsSubmitting } = useExitFormDialog({
formId
});
async function handleFormSubmit(data: TData): Promise<TErrors[]> {
setIsSubmitting(true);
const result = onSubmit(data);
if (!result) {
return [];
}
const errors = await result;
if (errors?.length === 0) {
setChanged(false);
return [];
}
setIsSubmitting(false);
return errors;
}
return handleFormSubmit;
}
export default useHandleFormSubmit;

View file

@ -1,3 +1,5 @@
import { ExitFormDialogContext } from "@saleor/components/Form/ExitFormDialogProvider";
import { useContext } from "react";
import useRouter from "use-react-router"; import useRouter from "use-react-router";
export type UseNavigatorResult = ( export type UseNavigatorResult = (
@ -14,11 +16,18 @@ function useNavigator(): UseNavigatorResult {
history history
} = useRouter(); } = useRouter();
const { shouldBlockNavigation } = useContext(ExitFormDialogContext);
return ( return (
url: string, url: string,
{ replace = false, preserveQs = false, resetScroll = false } = {} { replace = false, preserveQs = false, resetScroll = false } = {}
) => { ) => {
if (shouldBlockNavigation()) {
return;
}
const targetUrl = preserveQs ? url + search : url; const targetUrl = preserveQs ? url + search : url;
if (replace) { if (replace) {
history.replace(targetUrl); history.replace(targetUrl);
} else { } else {

View file

@ -35,6 +35,7 @@ import useAppChannel, {
AppChannelProvider AppChannelProvider
} from "./components/AppLayout/AppChannelContext"; } from "./components/AppLayout/AppChannelContext";
import { DateProvider } from "./components/Date"; import { DateProvider } from "./components/Date";
import ExitFormDialogProvider from "./components/Form/ExitFormDialogProvider";
import { LocaleProvider } from "./components/Locale"; import { LocaleProvider } from "./components/Locale";
import MessageManagerProvider from "./components/messages"; import MessageManagerProvider from "./components/messages";
import { ShopProvider } from "./components/Shop"; import { ShopProvider } from "./components/Shop";
@ -137,7 +138,9 @@ const App: React.FC = () => (
<ShopProvider> <ShopProvider>
<AppChannelProvider> <AppChannelProvider>
<ExternalAppProvider> <ExternalAppProvider>
<ExitFormDialogProvider>
<Routes /> <Routes />
</ExitFormDialogProvider>
</ExternalAppProvider> </ExternalAppProvider>
</AppChannelProvider> </AppChannelProvider>
</ShopProvider> </ShopProvider>

View file

@ -1,7 +1,11 @@
import { ConfirmButtonTransitionState, ThemeType } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState, ThemeType } from "@saleor/macaw-ui";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { MutationFunction, MutationResult } from "react-apollo"; import {
MutationFetchResult,
MutationFunction,
MutationResult
} from "react-apollo";
import { IntlShape } from "react-intl"; import { IntlShape } from "react-intl";
import { MultiAutocompleteChoiceType } from "./components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "./components/MultiAutocompleteSelectField";
@ -237,21 +241,46 @@ export function getMutationState(
return "default"; return "default";
} }
interface SaleorMutationResult { export interface SaleorMutationResult {
errors?: UserError[]; errors?: any[];
} }
export function getMutationErrors<
TData extends Record<string, SaleorMutationResult> type InferPromiseResult<T> = T extends Promise<infer V> ? V : never;
>(data: TData): UserError[] {
return Object.values(data).reduce( export const extractMutationErrors = async <
(acc: UserError[], mut) => [...acc, ...maybe(() => mut.errors, [])], TData extends InferPromiseResult<TPromise>,
[] TPromise extends Promise<MutationFetchResult<TData>>,
); TErrors extends ReturnType<typeof getMutationErrors>
>(
submitPromise: TPromise
): Promise<TErrors> => {
const result = await submitPromise;
const e = getMutationErrors(result);
return e as TErrors;
};
export const getMutationErrors = <
T extends MutationFetchResult<any>,
TData extends T["data"],
TErrors extends TData[keyof TData]["errors"]
>(
result: T
): TErrors[] => {
if (!result?.data) {
return [] as TErrors;
} }
return Object.values(result.data).reduce(
(acc: TErrors[], mut: TData) => [...acc, ...(mut.errors || [])],
[] as TErrors[]
) as TErrors;
};
export function getMutationStatus< export function getMutationStatus<
TData extends Record<string, SaleorMutationResult | any> TData extends Record<string, SaleorMutationResult | any>
>(opts: MutationResult<TData>): ConfirmButtonTransitionState { >(opts: MutationResult<TData>): ConfirmButtonTransitionState {
const errors = opts.data ? getMutationErrors(opts.data) : []; const errors = getMutationErrors(opts);
return getMutationState(opts.called, opts.loading, errors); return getMutationState(opts.called, opts.loading, errors);
} }

View file

@ -82,7 +82,7 @@ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
}; };
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <Form confirmLeave initial={initialForm} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => ( {({ change, data, hasChanged, submit }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>

View file

@ -12,7 +12,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { categoryUrl } from "../../../categories/urls"; import { categoryUrl } from "../../../categories/urls";
import { collectionUrl } from "../../../collections/urls"; import { collectionUrl } from "../../../collections/urls";
import { maybe } from "../../../misc"; import { extractMutationErrors, maybe } from "../../../misc";
import { pageUrl } from "../../../pages/urls"; import { pageUrl } from "../../../pages/urls";
import MenuDetailsPage, { import MenuDetailsPage, {
MenuDetailsSubmitData MenuDetailsSubmitData
@ -191,7 +191,11 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
open={params.action === "remove"} open={params.action === "remove"}
onClose={closeModal} onClose={closeModal}
confirmButtonState={menuDeleteOpts.status} confirmButtonState={menuDeleteOpts.status}
onConfirm={() => menuDelete({ variables: { id } })} onConfirm={() =>
extractMutationErrors(
menuDelete({ variables: { id } })
)
}
variant="delete" variant="delete"
title={intl.formatMessage({ title={intl.formatMessage({
defaultMessage: "Delete Menu", defaultMessage: "Delete Menu",
@ -227,7 +231,9 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
input: getMenuItemCreateInputData(id, data) input: getMenuItemCreateInputData(id, data)
}; };
menuItemCreate({ variables }); extractMutationErrors(
menuItemCreate({ variables })
);
}; };
return ( return (
@ -268,7 +274,9 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
input: getMenuItemInputData(data) input: getMenuItemInputData(data)
}; };
menuItemUpdate({ variables }); extractMutationErrors(
menuItemUpdate({ variables })
);
}; };
const menuItem = maybe(() => const menuItem = maybe(() =>

View file

@ -150,7 +150,7 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
{user === undefined ? ( {user === undefined ? (
<Skeleton /> <Skeleton />
) : isInEditMode && canEditCustomer ? ( ) : isInEditMode && canEditCustomer ? (
<Form initial={{ query: "" }}> <Form confirmLeave initial={{ query: "" }}>
{({ change, data }) => { {({ change, data }) => {
const handleChange = (event: React.ChangeEvent<any>) => { const handleChange = (event: React.ChangeEvent<any>) => {
change(event); change(event);

View file

@ -58,7 +58,7 @@ export interface OrderCustomerAddressesEditDialogProps {
defaultShippingAddress?: CustomerAddresses_user_defaultShippingAddress; defaultShippingAddress?: CustomerAddresses_user_defaultShippingAddress;
defaultBillingAddress?: CustomerAddresses_user_defaultBillingAddress; defaultBillingAddress?: CustomerAddresses_user_defaultBillingAddress;
onClose(); onClose();
onConfirm(data: OrderCustomerAddressesEditDialogOutput): SubmitPromise; onConfirm(data: OrderCustomerAddressesEditDialogOutput): SubmitPromise<any[]>;
} }
const defaultSearchState: OrderCustomerSearchAddressState = { const defaultSearchState: OrderCustomerSearchAddressState = {
@ -133,8 +133,13 @@ const OrderCustomerAddressesEditDialog: React.FC<OrderCustomerAddressesEditDialo
const adressesInput = handleAddressesSubmit(data); const adressesInput = handleAddressesSubmit(data);
if (adressesInput.shippingAddress && adressesInput.billingAddress) { if (adressesInput.shippingAddress && adressesInput.billingAddress) {
onConfirm(adressesInput); onConfirm(adressesInput as OrderCustomerAddressesEditDialogOutput);
} }
return Promise.resolve([
...shippingValidationErrors,
...billingValidationErrors
]);
}; };
const countryChoices = mapCountriesToChoices(countries); const countryChoices = mapCountriesToChoices(countries);

View file

@ -1,3 +1,4 @@
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { AddressTypeInput } from "@saleor/customers/types"; import { AddressTypeInput } from "@saleor/customers/types";
import { import {
@ -5,9 +6,14 @@ import {
CustomerAddresses_user_defaultBillingAddress, CustomerAddresses_user_defaultBillingAddress,
CustomerAddresses_user_defaultShippingAddress CustomerAddresses_user_defaultShippingAddress
} from "@saleor/customers/types/CustomerAddresses"; } from "@saleor/customers/types/CustomerAddresses";
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, {
CommonUseFormResultWithHandlers,
FormChange,
SubmitPromise
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
export enum AddressInputOptionEnum { export enum AddressInputOptionEnum {
CUSTOMER_ADDRESS = "customerAddress", CUSTOMER_ADDRESS = "customerAddress",
@ -43,12 +49,12 @@ export interface OrderCustomerAddressesEditHandlers {
selectBillingCountry: FormChange; selectBillingCountry: FormChange;
} }
interface UseOrderCustomerAddressesEditFormResult { interface UseOrderCustomerAddressesEditFormResult
submit: (event: React.FormEvent<any>) => void; extends CommonUseFormResultWithHandlers<
change: FormChange; OrderCustomerAddressesEditData,
hasChanged: boolean; OrderCustomerAddressesEditHandlers
data: OrderCustomerAddressesEditData; > {
handlers: OrderCustomerAddressesEditHandlers; submit: (event: React.FormEvent<any>) => SubmitPromise<any[]>;
} }
interface UseOrderCustomerAddressesEditFormOpts { interface UseOrderCustomerAddressesEditFormOpts {
@ -61,14 +67,9 @@ export interface OrderCustomerAddressesEditFormProps
extends UseOrderCustomerAddressesEditFormOpts { extends UseOrderCustomerAddressesEditFormOpts {
children: (props: UseOrderCustomerAddressesEditFormResult) => React.ReactNode; children: (props: UseOrderCustomerAddressesEditFormResult) => React.ReactNode;
initial?: Partial<OrderCustomerAddressesEditFormData>; initial?: Partial<OrderCustomerAddressesEditFormData>;
onSubmit: (data: OrderCustomerAddressesEditData) => void; onSubmit: (data: OrderCustomerAddressesEditData) => SubmitPromise<any[]>;
} }
function useOrderCustomerAddressesEditForm(
initial: Partial<OrderCustomerAddressesEditFormData>,
onSubmit: (data: OrderCustomerAddressesEditData) => void,
opts: UseOrderCustomerAddressesEditFormOpts
): UseOrderCustomerAddressesEditFormResult {
const initialAddress: AddressTypeInput = { const initialAddress: AddressTypeInput = {
city: "", city: "",
country: "", country: "",
@ -76,7 +77,10 @@ function useOrderCustomerAddressesEditForm(
postalCode: "", postalCode: "",
streetAddress1: "" streetAddress1: ""
}; };
const defaultInitialFormData: OrderCustomerAddressesEditFormData = {
const getDefaultInitialFormData = (
opts: UseOrderCustomerAddressesEditFormOpts
): OrderCustomerAddressesEditFormData => ({
billingSameAsShipping: true, billingSameAsShipping: true,
shippingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, shippingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS,
billingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, billingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS,
@ -84,15 +88,25 @@ function useOrderCustomerAddressesEditForm(
customerBillingAddress: opts.defaultBillingAddress, customerBillingAddress: opts.defaultBillingAddress,
shippingAddress: initialAddress, shippingAddress: initialAddress,
billingAddress: initialAddress billingAddress: initialAddress
};
const form = useForm({
...initial,
...defaultInitialFormData
}); });
const [changed, setChanged] = useState(false); function useOrderCustomerAddressesEditForm(
const triggerChange = () => setChanged(true); initial: Partial<OrderCustomerAddressesEditFormData>,
onSubmit: (data: OrderCustomerAddressesEditData) => SubmitPromise<any[]>,
opts: UseOrderCustomerAddressesEditFormOpts
): UseOrderCustomerAddressesEditFormResult {
const {
handleChange,
hasChanged,
change,
data: formData,
setChanged
} = useForm({
...initial,
...getDefaultInitialFormData(opts)
});
const { setExitDialogSubmitRef } = useExitFormDialog();
const [shippingCountryDisplayName, setShippingCountryDisplayName] = useState( const [shippingCountryDisplayName, setShippingCountryDisplayName] = useState(
"" ""
@ -101,19 +115,15 @@ function useOrderCustomerAddressesEditForm(
"" ""
); );
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const handleFormAddressChange = ( const handleFormAddressChange = (
event: React.ChangeEvent<any>, event: React.ChangeEvent<any>,
addressType: "shippingAddress" | "billingAddress" addressType: "shippingAddress" | "billingAddress"
) => ) =>
form.change({ change({
target: { target: {
name: addressType, name: addressType,
value: { value: {
...form.data[addressType], ...formData[addressType],
[event.target.name]: event.target.value [event.target.name]: event.target.value
} }
} }
@ -122,7 +132,7 @@ function useOrderCustomerAddressesEditForm(
customerAddress: CustomerAddresses_user_addresses, customerAddress: CustomerAddresses_user_addresses,
addressType: "customerShippingAddress" | "customerBillingAddress" addressType: "customerShippingAddress" | "customerBillingAddress"
) => ) =>
form.change({ change({
target: { target: {
name: addressType, name: addressType,
value: customerAddress value: customerAddress
@ -130,11 +140,11 @@ function useOrderCustomerAddressesEditForm(
}); });
const handleShippingCountrySelect = createSingleAutocompleteSelectHandler( const handleShippingCountrySelect = createSingleAutocompleteSelectHandler(
event => event =>
form.change({ change({
target: { target: {
name: "shippingAddress", name: "shippingAddress",
value: { value: {
...form.data.shippingAddress, ...formData.shippingAddress,
[event.target.name]: event.target.value [event.target.name]: event.target.value
} }
} }
@ -144,11 +154,11 @@ function useOrderCustomerAddressesEditForm(
); );
const handleBillingCountrySelect = createSingleAutocompleteSelectHandler( const handleBillingCountrySelect = createSingleAutocompleteSelectHandler(
event => event =>
form.change({ change({
target: { target: {
name: "billingAddress", name: "billingAddress",
value: { value: {
...form.data.billingAddress, ...formData.billingAddress,
[event.target.name]: event.target.value [event.target.name]: event.target.value
} }
} }
@ -158,21 +168,30 @@ function useOrderCustomerAddressesEditForm(
); );
const data = { const data = {
...form.data, ...formData,
shippingCountryDisplayName, shippingCountryDisplayName,
billingCountryDisplayName billingCountryDisplayName
}; };
const handleFormSubmit = useHandleFormSubmit({
onSubmit,
setChanged
});
const handleSubmit = () => handleFormSubmit(data);
const submit = (event: React.FormEvent<any>) => { const submit = (event: React.FormEvent<any>) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
return onSubmit(data); return handleSubmit();
}; };
useEffect(() => setExitDialogSubmitRef(submit), [handleSubmit]);
return { return {
change: handleChange, change: handleChange,
submit, submit,
hasChanged: changed, hasChanged,
data, data,
handlers: { handlers: {
changeCustomerAddress: handleCustomerAddressChange, changeCustomerAddress: handleCustomerAddressChange,

View file

@ -23,8 +23,8 @@ import { useStyles } from "./styles";
export interface OrderCustomerChangeDialogProps { export interface OrderCustomerChangeDialogProps {
open: boolean; open: boolean;
onConfirm: (data: OrderCustomerChangeData) => void;
onClose(); onClose();
onConfirm(data: OrderCustomerChangeData): void;
} }
const OrderCustomerChangeDialog: React.FC<OrderCustomerChangeDialogProps> = props => { const OrderCustomerChangeDialog: React.FC<OrderCustomerChangeDialogProps> = props => {

View file

@ -1,5 +1,6 @@
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, { CommonUseFormResult } from "@saleor/hooks/useForm";
import React, { useState } from "react"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import React from "react";
export enum CustomerChangeActionEnum { export enum CustomerChangeActionEnum {
KEEP_ADDRESS = "keepAddress", KEEP_ADDRESS = "keepAddress",
@ -10,12 +11,9 @@ export interface OrderCustomerChangeData {
changeActionOption: CustomerChangeActionEnum; changeActionOption: CustomerChangeActionEnum;
} }
interface UseOrderCustomerChangeFormResult { type UseOrderCustomerChangeFormResult = CommonUseFormResult<
submit: (event: React.FormEvent<any>) => void; OrderCustomerChangeData
change: FormChange; >;
hasChanged: boolean;
data: OrderCustomerChangeData;
}
export interface OrderCustomerChangeFormProps { export interface OrderCustomerChangeFormProps {
children: (props: UseOrderCustomerChangeFormResult) => React.ReactNode; children: (props: UseOrderCustomerChangeFormResult) => React.ReactNode;
@ -23,38 +21,37 @@ export interface OrderCustomerChangeFormProps {
onSubmit: (data: OrderCustomerChangeData) => void; onSubmit: (data: OrderCustomerChangeData) => void;
} }
function useOrderCustomerChangeForm(
initial: Partial<OrderCustomerChangeData>,
onSubmit: (data: OrderCustomerChangeData) => void
): UseOrderCustomerChangeFormResult {
const defaultInitialFormData: OrderCustomerChangeData = { const defaultInitialFormData: OrderCustomerChangeData = {
changeActionOption: CustomerChangeActionEnum.KEEP_ADDRESS changeActionOption: CustomerChangeActionEnum.KEEP_ADDRESS
}; };
const form = useForm({ function useOrderCustomerChangeForm(
initial: Partial<OrderCustomerChangeData> = {},
onSubmit: (data: OrderCustomerChangeData) => void
): UseOrderCustomerChangeFormResult {
const { handleChange, hasChanged, data, setChanged } = useForm({
...initial, ...initial,
...defaultInitialFormData ...defaultInitialFormData
}); });
const [changed, setChanged] = useState(false); const handleFormSubmit = useHandleFormSubmit({
const triggerChange = () => setChanged(true); onSubmit,
setChanged
});
const handleChange: FormChange = (event, cb) => { const handleSubmit = () => handleFormSubmit(data);
form.change(event, cb);
triggerChange();
};
const submit = (event: React.FormEvent<any>) => { const submit = (event: React.FormEvent<any>) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
return onSubmit(form.data); return handleSubmit();
}; };
return { return {
change: handleChange, change: handleChange,
submit, submit,
hasChanged: changed, hasChanged,
data: form.data data
}; };
} }
@ -63,7 +60,7 @@ const OrderCustomerChangeForm: React.FC<OrderCustomerChangeFormProps> = ({
initial, initial,
onSubmit onSubmit
}) => { }) => {
const props = useOrderCustomerChangeForm(initial || {}, onSubmit); const props = useOrderCustomerChangeForm(initial, onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -22,7 +22,7 @@ import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTr
import React from "react"; import React from "react";
import { defineMessages, useIntl } from "react-intl"; import { defineMessages, useIntl } from "react-intl";
import { maybe } from "../../../misc"; import { getMutationErrors, maybe } from "../../../misc";
import { OrderStatus } from "../../../types/globalTypes"; import { OrderStatus } from "../../../types/globalTypes";
import { import {
OrderDetails_order, OrderDetails_order,
@ -173,7 +173,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
privateMetadata privateMetadata
}); });
resetMetadataChanged(); resetMetadataChanged();
return result; return getMutationErrors(result);
}; };
const initial: MetadataFormData = { const initial: MetadataFormData = {
@ -212,7 +212,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
]); ]);
return ( return (
<Form initial={initial} onSubmit={handleSubmit}> <Form confirmLeave initial={initial} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);

View file

@ -7,6 +7,7 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -50,7 +51,7 @@ export interface OrderDraftPageProps
onCustomerEdit: (data: CustomerEditData) => void; onCustomerEdit: (data: CustomerEditData) => void;
onDraftFinalize: () => void; onDraftFinalize: () => void;
onDraftRemove: () => void; onDraftRemove: () => void;
onNoteAdd: (data: HistoryFormData) => void; onNoteAdd: (data: HistoryFormData) => SubmitPromise<any[]>;
onOrderLineAdd: () => void; onOrderLineAdd: () => void;
onOrderLineChange: ( onOrderLineChange: (
id: string, id: string,

View file

@ -20,6 +20,7 @@ import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { ShopOrderSettingsFragment } from "@saleor/fragments/types/ShopOrderSettingsFragment"; import { ShopOrderSettingsFragment } from "@saleor/fragments/types/ShopOrderSettingsFragment";
import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, { FormsetData } from "@saleor/hooks/useFormset"; import useFormset, { FormsetData } from "@saleor/hooks/useFormset";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
@ -110,7 +111,7 @@ const useStyles = makeStyles(
interface OrderFulfillFormData { interface OrderFulfillFormData {
sendInfo: boolean; sendInfo: boolean;
} }
interface OrderFulfillSubmitData extends OrderFulfillFormData { export interface OrderFulfillSubmitData extends OrderFulfillFormData {
items: FormsetData<null, OrderFulfillStockInput[]>; items: FormsetData<null, OrderFulfillStockInput[]>;
} }
export interface OrderFulfillPageProps { export interface OrderFulfillPageProps {
@ -121,7 +122,7 @@ export interface OrderFulfillPageProps {
warehouses: WarehouseFragment[]; warehouses: WarehouseFragment[];
shopSettings?: ShopOrderSettingsFragment; shopSettings?: ShopOrderSettingsFragment;
onBack: () => void; onBack: () => void;
onSubmit: (data: OrderFulfillSubmitData) => void; onSubmit: (data: OrderFulfillSubmitData) => SubmitPromise;
} }
const initialFormData: OrderFulfillFormData = { const initialFormData: OrderFulfillFormData = {
@ -220,7 +221,7 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
orderNumber: order?.number orderNumber: order?.number
})} })}
/> />
<Form initial={initialFormData} onSubmit={handleSubmit}> <Form confirmLeave initial={initialFormData} onSubmit={handleSubmit}>
{({ change, data, submit }) => ( {({ change, data, submit }) => (
<> <>
<Card> <Card>

View file

@ -76,7 +76,7 @@ const OrderFulfillmentCancelDialog: React.FC<OrderFulfillmentCancelDialogProps>
fullWidth fullWidth
maxWidth="sm" maxWidth="sm"
> >
<Form initial={{ warehouseId: null }} onSubmit={onConfirm}> <Form confirmLeave initial={{ warehouseId: null }} onSubmit={onConfirm}>
{({ change, data: formData, submit }) => { {({ change, data: formData, submit }) => {
const handleChange = createSingleAutocompleteSelectHandler( const handleChange = createSingleAutocompleteSelectHandler(
change, change,

View file

@ -48,7 +48,7 @@ const OrderFulfillmentTrackingDialog: React.FC<OrderFulfillmentTrackingDialogPro
return ( return (
<Dialog onClose={onClose} open={open} fullWidth maxWidth="xs"> <Dialog onClose={onClose} open={open} fullWidth maxWidth="xs">
<Form initial={{ trackingNumber }} onSubmit={onConfirm}> <Form confirmLeave initial={{ trackingNumber }} onSubmit={onConfirm}>
{({ change, data, submit }) => ( {({ change, data, submit }) => (
<> <>
<DialogTitle> <DialogTitle>

View file

@ -9,6 +9,7 @@ import {
TimelineEventProps, TimelineEventProps,
TimelineNote TimelineNote
} from "@saleor/components/Timeline"; } from "@saleor/components/Timeline";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { makeStyles } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
import { OrderDetails_order_events } from "@saleor/orders/types/OrderDetails"; import { OrderDetails_order_events } from "@saleor/orders/types/OrderDetails";
import { import {
@ -295,7 +296,7 @@ const useStyles = makeStyles(
interface OrderHistoryProps { interface OrderHistoryProps {
history: OrderDetails_order_events[]; history: OrderDetails_order_events[];
orderCurrency: string; orderCurrency: string;
onNoteAdd: (data: FormData) => void; onNoteAdd: (data: FormData) => SubmitPromise;
} }
const OrderHistory: React.FC<OrderHistoryProps> = props => { const OrderHistory: React.FC<OrderHistoryProps> = props => {
@ -331,7 +332,12 @@ const OrderHistory: React.FC<OrderHistoryProps> = props => {
<Hr /> <Hr />
{history ? ( {history ? (
<Timeline> <Timeline>
<Form initial={{ message: "" }} onSubmit={onNoteAdd} resetOnSubmit> <Form
confirmLeave
initial={{ message: "" }}
onSubmit={onNoteAdd}
resetOnSubmit
>
{({ change, data, reset, submit }) => ( {({ change, data, reset, submit }) => (
<TimelineAddNote <TimelineAddNote
message={data.message} message={data.message}

View file

@ -1,11 +1,15 @@
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import useForm, {
CommonUseFormResultWithHandlers,
SubmitPromise
} from "@saleor/hooks/useForm";
import useFormset, { import useFormset, {
FormsetChange, FormsetChange,
FormsetData FormsetData
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { OrderRefundData_order } from "@saleor/orders/types/OrderRefundData"; import { OrderRefundData_order } from "@saleor/orders/types/OrderRefundData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import React, { useEffect } from "react";
import React from "react";
import { refundFulfilledStatuses } from "./OrderRefundPage"; import { refundFulfilledStatuses } from "./OrderRefundPage";
@ -40,13 +44,12 @@ export interface OrderRefundFormData extends OrderRefundData {
export type OrderRefundSubmitData = OrderRefundFormData; export type OrderRefundSubmitData = OrderRefundFormData;
export interface UseOrderRefundFormResult { export interface UseOrderRefundFormResult
change: FormChange; extends CommonUseFormResultWithHandlers<
data: OrderRefundFormData; OrderRefundFormData,
OrderRefundHandlers
> {
disabled: boolean; disabled: boolean;
handlers: OrderRefundHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
} }
interface OrderRefundFormProps { interface OrderRefundFormProps {
@ -72,10 +75,19 @@ function useOrderRefundForm(
defaultType: OrderRefundType, defaultType: OrderRefundType,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise onSubmit: (data: OrderRefundSubmitData) => SubmitPromise
): UseOrderRefundFormResult { ): UseOrderRefundFormResult {
const [changed, setChanged] = React.useState(false); const {
const triggerChange = () => setChanged(true); handleChange,
setChanged,
hasChanged,
triggerChange,
data: formData,
formId
} = useForm(getOrderRefundPageFormData(defaultType), undefined, {
confirmLeave: true
});
const { setExitDialogSubmitRef } = useExitFormDialog();
const form = useForm(getOrderRefundPageFormData(defaultType));
const refundedProductQuantities = useFormset<null, string>( const refundedProductQuantities = useFormset<null, string>(
order?.lines order?.lines
.filter(line => line.quantityToFulfill > 0) .filter(line => line.quantityToFulfill > 0)
@ -103,10 +115,6 @@ function useOrderRefundForm(
) )
); );
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const handleRefundedProductQuantityChange: FormsetChange<string> = ( const handleRefundedProductQuantityChange: FormsetChange<string> = (
id, id,
value value
@ -165,12 +173,20 @@ function useOrderRefundForm(
}; };
const data: OrderRefundFormData = { const data: OrderRefundFormData = {
...form.data, ...formData,
refundedFulfilledProductQuantities: refundedFulfilledProductQuantities.data, refundedFulfilledProductQuantities: refundedFulfilledProductQuantities.data,
refundedProductQuantities: refundedProductQuantities.data refundedProductQuantities: refundedProductQuantities.data
}; };
const submit = () => handleFormSubmit(data, onSubmit, setChanged); const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const submit = () => handleFormSubmit(data);
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const disabled = !order; const disabled = !order;
@ -184,7 +200,7 @@ function useOrderRefundForm(
setMaximalRefundedFulfilledProductQuantities: handleMaximalRefundedFulfilledProductQuantitiesSet, setMaximalRefundedFulfilledProductQuantities: handleMaximalRefundedFulfilledProductQuantitiesSet,
setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -1,12 +1,16 @@
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import useForm, {
CommonUseFormResultWithHandlers,
SubmitPromise
} from "@saleor/hooks/useForm";
import useFormset, { import useFormset, {
FormsetChange, FormsetChange,
FormsetData FormsetData
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails"; import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import { FulfillmentStatus } from "@saleor/types/globalTypes"; import { FulfillmentStatus } from "@saleor/types/globalTypes";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import React, { useEffect } from "react";
import React, { useState } from "react";
import { OrderRefundAmountCalculationMode } from "../OrderRefundPage/form"; import { OrderRefundAmountCalculationMode } from "../OrderRefundPage/form";
import { import {
@ -55,13 +59,10 @@ export interface OrderReturnFormData extends OrderReturnData {
export type OrderRefundSubmitData = OrderReturnFormData; export type OrderRefundSubmitData = OrderReturnFormData;
export interface UseOrderRefundFormResult { export type UseOrderRefundFormResult = CommonUseFormResultWithHandlers<
change: FormChange; OrderReturnFormData,
hasChanged: boolean; OrderReturnHandlers
data: OrderReturnFormData; >;
handlers: OrderReturnHandlers;
submit: () => Promise<boolean>;
}
interface OrderReturnProps { interface OrderReturnProps {
children: (props: UseOrderRefundFormResult) => React.ReactNode; children: (props: UseOrderRefundFormResult) => React.ReactNode;
@ -79,12 +80,18 @@ function useOrderReturnForm(
order: OrderDetails_order, order: OrderDetails_order,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise onSubmit: (data: OrderRefundSubmitData) => SubmitPromise
): UseOrderRefundFormResult { ): UseOrderRefundFormResult {
const form = useForm(getOrderRefundPageFormData()); const {
const [hasChanged, setHasChanged] = useState(false); handleChange,
setChanged,
hasChanged,
data: formData,
triggerChange,
formId
} = useForm(getOrderRefundPageFormData(), undefined, {
confirmLeave: true
});
const handleChange: FormChange = (event, cb) => { const { setExitDialogSubmitRef } = useExitFormDialog();
form.change(event, cb);
};
const unfulfiledItemsQuantites = useFormset<LineItemData, number>( const unfulfiledItemsQuantites = useFormset<LineItemData, number>(
getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 })) getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 }))
@ -215,12 +222,18 @@ function useOrderReturnForm(
waitingItemsQuantities: waitingItemsQuantities.data, waitingItemsQuantities: waitingItemsQuantities.data,
itemsToBeReplaced: itemsToBeReplaced.data, itemsToBeReplaced: itemsToBeReplaced.data,
unfulfilledItemsQuantities: unfulfiledItemsQuantites.data, unfulfilledItemsQuantities: unfulfiledItemsQuantites.data,
...form.data ...formData
}; };
const submit = () => handleFormSubmit(data, onSubmit, setHasChanged); const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const triggerChange = () => setHasChanged(true); const submit = () => handleFormSubmit(data);
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
function handleHandlerChange<T>(callback: (id: string, value: T) => void) { function handleHandlerChange<T>(callback: (id: string, value: T) => void) {
return (id: string, value: T) => { return (id: string, value: T) => {

View file

@ -5,7 +5,7 @@ import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { OrderSettingsFormData } from "../OrderSettingsPage/form"; import { OrderSettingsFormData } from "../OrderSettingsPage/types";
export interface OrderSettingsProps { export interface OrderSettingsProps {
data: OrderSettingsFormData; data: OrderSettingsFormData;

View file

@ -14,7 +14,8 @@ import { FormattedMessage, useIntl } from "react-intl";
import OrderFulfillmentSettings from "../OrderFulfillmentSettings"; import OrderFulfillmentSettings from "../OrderFulfillmentSettings";
import OrderSettings from "../OrderSettings/OrderSettings"; import OrderSettings from "../OrderSettings/OrderSettings";
import OrderSettingsForm, { OrderSettingsFormData } from "./form"; import OrderSettingsForm from "./form";
import { OrderSettingsFormData } from "./types";
export interface OrderSettingsPageProps { export interface OrderSettingsPageProps {
orderSettings: OrderSettingsFragment; orderSettings: OrderSettingsFragment;

View file

@ -1,7 +1,7 @@
import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment"; import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment";
import { ShopOrderSettingsFragment } from "@saleor/fragments/types/ShopOrderSettingsFragment"; import { ShopOrderSettingsFragment } from "@saleor/fragments/types/ShopOrderSettingsFragment";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import React from "react"; import React from "react";
export interface OrderSettingsFormData { export interface OrderSettingsFormData {
@ -15,7 +15,7 @@ export interface UseOrderSettingsFormResult {
change: FormChange; change: FormChange;
data: OrderSettingsFormData; data: OrderSettingsFormData;
hasChanged: boolean; hasChanged: boolean;
submit: () => Promise<boolean>; submit: () => SubmitPromise<any[]>;
} }
export interface OrderSettingsFormProps { export interface OrderSettingsFormProps {
@ -45,20 +45,20 @@ function useOrderSettingsForm(
onSubmit: (data: OrderSettingsFormData) => SubmitPromise onSubmit: (data: OrderSettingsFormData) => SubmitPromise
): UseOrderSettingsFormResult { ): UseOrderSettingsFormResult {
const [changed, setChanged] = React.useState(false); const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm(getOrderSeettingsFormData(orderSettings, shop)); const { data, handleChange, formId } = useForm(
getOrderSeettingsFormData(orderSettings, shop),
undefined,
{ confirmLeave: true }
);
const handleChange: FormChange = (event, cb) => { const handleFormSubmit = useHandleFormSubmit({
form.change(event, cb); formId,
triggerChange(); onSubmit,
}; setChanged
});
const data: OrderSettingsFormData = { const submit = () => handleFormSubmit(data);
...form.data
};
const submit = () => handleFormSubmit(form.data, onSubmit, setChanged);
return { return {
change: handleChange, change: handleChange,

View file

@ -0,0 +1,6 @@
export interface OrderSettingsFormData {
automaticallyConfirmAllNewOrders: boolean;
fulfillmentAutoApprove: boolean;
fulfillmentAllowUnpaid: boolean;
automaticallyFulfillNonShippableGiftCard: boolean;
}

View file

@ -0,0 +1,265 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { Task } from "@saleor/containers/BackgroundTasks/types";
import useBackgroundTask from "@saleor/hooks/useBackgroundTask";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { useOrderConfirmMutation } from "@saleor/orders/mutations";
import { InvoiceRequest } from "@saleor/orders/types/InvoiceRequest";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react";
import { useIntl } from "react-intl";
import { JobStatusEnum, OrderStatus } from "../../../types/globalTypes";
import OrderOperations from "../../containers/OrderOperations";
import { TypedOrderDetailsQuery } from "../../queries";
import {
orderListUrl,
orderUrl,
OrderUrlDialog,
OrderUrlQueryParams
} from "../../urls";
import OrderAddressFields from "./OrderAddressFields";
import { OrderDetailsMessages } from "./OrderDetailsMessages";
import { OrderDraftDetails } from "./OrderDraftDetails";
import { OrderNormalDetails } from "./OrderNormalDetails";
import { OrderUnconfirmedDetails } from "./OrderUnconfirmedDetails";
interface OrderDetailsProps {
id: string;
params: OrderUrlQueryParams;
}
export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
const navigate = useNavigator();
const { queue } = useBackgroundTask();
const intl = useIntl();
const [updateMetadata, updateMetadataOpts] = useMetadataUpdate({});
const [
updatePrivateMetadata,
updatePrivateMetadataOpts
] = usePrivateMetadataUpdate({});
const notify = useNotifier();
const [openModal, closeModal] = createDialogActionHandlers<
OrderUrlDialog,
OrderUrlQueryParams
>(navigate, params => orderUrl(id, params), params);
const handleBack = () => navigate(orderListUrl());
const [orderConfirm] = useOrderConfirmMutation({
onCompleted: ({ orderConfirm: { errors } }) => {
const isError = !!errors.length;
notify({
status: isError ? "error" : "success",
text: isError
? getOrderErrorMessage(errors[0], intl)
: "Confirmed Order"
});
}
});
return (
<TypedOrderDetailsQuery displayLoader variables={{ id }}>
{({ data, loading }) => {
const order = data?.order;
if (order === null) {
return <NotFoundPage onBack={handleBack} />;
}
const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED;
const isOrderDraft = order?.status === OrderStatus.DRAFT;
const handleSubmit = async (data: MetadataFormData) => {
if (order?.status === OrderStatus.UNCONFIRMED) {
await orderConfirm({ variables: { id: order?.id } });
}
const update = createMetadataUpdateHandler(
order,
() => Promise.resolve([]),
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const result = await update(data);
if (result.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
}
return result;
};
return (
<OrderDetailsMessages id={id} params={params}>
{orderMessages => (
<OrderOperations
order={id}
onNoteAdd={orderMessages.handleNoteAdd}
onOrderCancel={orderMessages.handleOrderCancel}
onOrderVoid={orderMessages.handleOrderVoid}
onPaymentCapture={orderMessages.handlePaymentCapture}
onUpdate={orderMessages.handleUpdate}
onDraftUpdate={orderMessages.handleDraftUpdate}
onShippingMethodUpdate={data => {
orderMessages.handleShippingMethodUpdate(data);
order.total = data.orderUpdateShipping.order.total;
}}
onOrderLineDelete={orderMessages.handleOrderLineDelete}
onOrderLinesAdd={orderMessages.handleOrderLinesAdd}
onOrderLineUpdate={orderMessages.handleOrderLineUpdate}
onOrderFulfillmentApprove={
orderMessages.handleOrderFulfillmentApprove
}
onOrderFulfillmentCancel={
orderMessages.handleOrderFulfillmentCancel
}
onOrderFulfillmentUpdate={
orderMessages.handleOrderFulfillmentUpdate
}
onDraftFinalize={orderMessages.handleDraftFinalize}
onDraftCancel={orderMessages.handleDraftCancel}
onOrderMarkAsPaid={orderMessages.handleOrderMarkAsPaid}
onInvoiceRequest={(data: InvoiceRequest) => {
if (
data.invoiceRequest.invoice.status === JobStatusEnum.SUCCESS
) {
orderMessages.handleInvoiceGenerateFinished(data);
} else {
orderMessages.handleInvoiceGeneratePending(data);
queue(Task.INVOICE_GENERATE, {
generateInvoice: {
invoiceId: data.invoiceRequest.invoice.id,
orderId: id
}
});
}
}}
onInvoiceSend={orderMessages.handleInvoiceSend}
>
{({
orderAddNote,
orderCancel,
orderDraftUpdate,
orderLinesAdd,
orderLineDelete,
orderLineUpdate,
orderPaymentCapture,
orderVoid,
orderShippingMethodUpdate,
orderUpdate,
orderFulfillmentApprove,
orderFulfillmentCancel,
orderFulfillmentUpdateTracking,
orderDraftCancel,
orderDraftFinalize,
orderPaymentMarkAsPaid,
orderInvoiceRequest,
orderInvoiceSend
}) => (
<>
{!isOrderDraft && !isOrderUnconfirmed && (
<OrderNormalDetails
id={id}
params={params}
data={data}
orderAddNote={orderAddNote}
orderInvoiceRequest={orderInvoiceRequest}
handleSubmit={handleSubmit}
orderCancel={orderCancel}
orderPaymentMarkAsPaid={orderPaymentMarkAsPaid}
orderVoid={orderVoid}
orderPaymentCapture={orderPaymentCapture}
orderFulfillmentApprove={orderFulfillmentApprove}
orderFulfillmentCancel={orderFulfillmentCancel}
orderFulfillmentUpdateTracking={
orderFulfillmentUpdateTracking
}
orderInvoiceSend={orderInvoiceSend}
updateMetadataOpts={updateMetadataOpts}
updatePrivateMetadataOpts={updatePrivateMetadataOpts}
openModal={openModal}
closeModal={closeModal}
/>
)}
{isOrderDraft && (
<OrderDraftDetails
id={id}
params={params}
loading={loading}
data={data}
orderAddNote={orderAddNote}
orderLineUpdate={orderLineUpdate}
orderLineDelete={orderLineDelete}
orderShippingMethodUpdate={orderShippingMethodUpdate}
orderLinesAdd={orderLinesAdd}
orderDraftUpdate={orderDraftUpdate}
orderDraftCancel={orderDraftCancel}
orderDraftFinalize={orderDraftFinalize}
openModal={openModal}
closeModal={closeModal}
/>
)}
{isOrderUnconfirmed && (
<OrderUnconfirmedDetails
id={id}
params={params}
data={data}
orderAddNote={orderAddNote}
orderLineUpdate={orderLineUpdate}
orderLineDelete={orderLineDelete}
orderInvoiceRequest={orderInvoiceRequest}
handleSubmit={handleSubmit}
orderCancel={orderCancel}
orderShippingMethodUpdate={orderShippingMethodUpdate}
orderLinesAdd={orderLinesAdd}
orderPaymentMarkAsPaid={orderPaymentMarkAsPaid}
orderVoid={orderVoid}
orderPaymentCapture={orderPaymentCapture}
orderFulfillmentApprove={orderFulfillmentApprove}
orderFulfillmentCancel={orderFulfillmentCancel}
orderFulfillmentUpdateTracking={
orderFulfillmentUpdateTracking
}
orderInvoiceSend={orderInvoiceSend}
updateMetadataOpts={updateMetadataOpts}
updatePrivateMetadataOpts={updatePrivateMetadataOpts}
openModal={openModal}
closeModal={closeModal}
/>
)}
<OrderAddressFields
isDraft={order?.status === OrderStatus.DRAFT}
orderUpdate={orderUpdate}
orderDraftUpdate={orderDraftUpdate}
data={data}
id={id}
onClose={closeModal}
action={params.action}
/>
</>
)}
</OrderOperations>
)}
</OrderDetailsMessages>
);
}}
</TypedOrderDetailsQuery>
);
};
export default OrderDetails;

View file

@ -22,7 +22,10 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { customerUrl } from "../../../../customers/urls"; import { customerUrl } from "../../../../customers/urls";
import { getStringOrPlaceholder } from "../../../../misc"; import {
extractMutationErrors,
getStringOrPlaceholder
} from "../../../../misc";
import { productUrl } from "../../../../products/urls"; import { productUrl } from "../../../../products/urls";
import OrderDraftCancelDialog from "../../../components/OrderDraftCancelDialog/OrderDraftCancelDialog"; import OrderDraftCancelDialog from "../../../components/OrderDraftCancelDialog/OrderDraftCancelDialog";
import OrderDraftPage from "../../../components/OrderDraftPage"; import OrderDraftPage from "../../../components/OrderDraftPage";
@ -172,10 +175,12 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
<OrderDraftPage <OrderDraftPage
disabled={loading} disabled={loading}
onNoteAdd={variables => onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({ orderAddNote.mutate({
input: variables, input: variables,
order: id order: id
}) })
)
} }
users={mapEdgesToItems(users?.data?.search)} users={mapEdgesToItems(users?.data?.search)}
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false} hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
@ -245,6 +250,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
onFetch={variantSearch} onFetch={variantSearch}
onFetchMore={loadMore} onFetchMore={loadMore}
onSubmit={variants => onSubmit={variants =>
extractMutationErrors(
orderLinesAdd.mutate({ orderLinesAdd.mutate({
id, id,
input: variants.map(variant => ({ input: variants.map(variant => ({
@ -252,6 +258,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
variantId: variant.id variantId: variant.id
})) }))
}) })
)
} }
/> />
<OrderCustomerChangeDialog <OrderCustomerChangeDialog

View file

@ -15,7 +15,11 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { customerUrl } from "../../../../customers/urls"; import { customerUrl } from "../../../../customers/urls";
import { getMutationState, getStringOrPlaceholder } from "../../../../misc"; import {
extractMutationErrors,
getMutationState,
getStringOrPlaceholder
} from "../../../../misc";
import { productUrl } from "../../../../products/urls"; import { productUrl } from "../../../../products/urls";
import { FulfillmentStatus } from "../../../../types/globalTypes"; import { FulfillmentStatus } from "../../../../types/globalTypes";
import OrderCancelDialog from "../../../components/OrderCancelDialog"; import OrderCancelDialog from "../../../components/OrderCancelDialog";
@ -113,10 +117,12 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
} }
onNoteAdd={variables => onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({ orderAddNote.mutate({
input: variables, input: variables,
order: id order: id
}) })
)
} }
onBack={handleBack} onBack={handleBack}
order={order} order={order}

View file

@ -18,7 +18,11 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { customerUrl } from "../../../../customers/urls"; import { customerUrl } from "../../../../customers/urls";
import { getMutationState, getStringOrPlaceholder } from "../../../../misc"; import {
extractMutationErrors,
getMutationState,
getStringOrPlaceholder
} from "../../../../misc";
import { productUrl } from "../../../../products/urls"; import { productUrl } from "../../../../products/urls";
import { FulfillmentStatus } from "../../../../types/globalTypes"; import { FulfillmentStatus } from "../../../../types/globalTypes";
import OrderCancelDialog from "../../../components/OrderCancelDialog"; import OrderCancelDialog from "../../../components/OrderCancelDialog";
@ -136,10 +140,12 @@ export const OrderUnconfirmedDetails: React.FC<OrderUnconfirmedDetailsProps> = (
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
} }
onNoteAdd={variables => onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({ orderAddNote.mutate({
input: variables, input: variables,
order: id order: id
}) })
)
} }
onBack={handleBack} onBack={handleBack}
order={order} order={order}
@ -248,12 +254,14 @@ export const OrderUnconfirmedDetails: React.FC<OrderUnconfirmedDetailsProps> = (
shippingMethods={order?.shippingMethods} shippingMethods={order?.shippingMethods}
onClose={closeModal} onClose={closeModal}
onSubmit={variables => onSubmit={variables =>
extractMutationErrors(
orderShippingMethodUpdate.mutate({ orderShippingMethodUpdate.mutate({
id, id,
input: { input: {
shippingMethod: variables.shippingMethod shippingMethod: variables.shippingMethod
} }
}) })
)
} }
/> />
<OrderProductAddDialog <OrderProductAddDialog

View file

@ -1,289 +1,2 @@
import { MetadataFormData } from "@saleor/components/Metadata"; export * from "./OrderDetails";
import NotFoundPage from "@saleor/components/NotFoundPage"; export { default } from "./OrderDetails";
import { Task } from "@saleor/containers/BackgroundTasks/types";
import useBackgroundTask from "@saleor/hooks/useBackgroundTask";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { useOrderConfirmMutation } from "@saleor/orders/mutations";
import { InvoiceRequest } from "@saleor/orders/types/InvoiceRequest";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react";
import { useIntl } from "react-intl";
import {
InvoiceErrorCode,
JobStatusEnum,
OrderStatus
} from "../../../types/globalTypes";
import OrderOperations from "../../containers/OrderOperations";
import { TypedOrderDetailsQuery } from "../../queries";
import {
orderListUrl,
orderUrl,
OrderUrlDialog,
OrderUrlQueryParams
} from "../../urls";
import OrderAddressFields from "./OrderAddressFields";
import { OrderDetailsMessages } from "./OrderDetailsMessages";
import { OrderDraftDetails } from "./OrderDraftDetails";
import { OrderNormalDetails } from "./OrderNormalDetails";
import { OrderUnconfirmedDetails } from "./OrderUnconfirmedDetails";
interface OrderDetailsProps {
id: string;
params: OrderUrlQueryParams;
}
export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
const navigate = useNavigator();
const { queue } = useBackgroundTask();
const intl = useIntl();
const [updateMetadata, updateMetadataOpts] = useMetadataUpdate({});
const [
updatePrivateMetadata,
updatePrivateMetadataOpts
] = usePrivateMetadataUpdate({});
const notify = useNotifier();
const [openModal, closeModal] = createDialogActionHandlers<
OrderUrlDialog,
OrderUrlQueryParams
>(navigate, params => orderUrl(id, params), params);
const handleBack = () => navigate(orderListUrl());
const [orderConfirm] = useOrderConfirmMutation({
onCompleted: ({ orderConfirm: { errors } }) => {
const isError = !!errors.length;
notify({
status: isError ? "error" : "success",
text: isError
? getOrderErrorMessage(errors[0], intl)
: "Confirmed Order"
});
}
});
return (
<TypedOrderDetailsQuery displayLoader variables={{ id }}>
{({ data, loading }) => {
const order = data?.order;
if (order === null) {
return <NotFoundPage onBack={handleBack} />;
}
const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED;
const isOrderDraft = order?.status === OrderStatus.DRAFT;
const handleSubmit = async (data: MetadataFormData) => {
if (order?.status === OrderStatus.UNCONFIRMED) {
await orderConfirm({ variables: { id: order?.id } });
}
const update = createMetadataUpdateHandler(
{
id: order.token,
metadata: order.metadata,
privateMetadata: order.privateMetadata
},
() => Promise.resolve([]),
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const result = await update(data);
if (result.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
}
return result;
};
return (
<OrderDetailsMessages id={id} params={params}>
{orderMessages => (
<OrderOperations
order={id}
onNoteAdd={orderMessages.handleNoteAdd}
onOrderCancel={orderMessages.handleOrderCancel}
onOrderVoid={orderMessages.handleOrderVoid}
onPaymentCapture={orderMessages.handlePaymentCapture}
onUpdate={orderMessages.handleUpdate}
onDraftUpdate={orderMessages.handleDraftUpdate}
onShippingMethodUpdate={data => {
orderMessages.handleShippingMethodUpdate(data);
order.total = data.orderUpdateShipping.order.total;
}}
onOrderLineDelete={orderMessages.handleOrderLineDelete}
onOrderLinesAdd={orderMessages.handleOrderLinesAdd}
onOrderLineUpdate={orderMessages.handleOrderLineUpdate}
onOrderFulfillmentApprove={
orderMessages.handleOrderFulfillmentApprove
}
onOrderFulfillmentCancel={
orderMessages.handleOrderFulfillmentCancel
}
onOrderFulfillmentUpdate={
orderMessages.handleOrderFulfillmentUpdate
}
onDraftFinalize={orderMessages.handleDraftFinalize}
onDraftCancel={orderMessages.handleDraftCancel}
onOrderMarkAsPaid={orderMessages.handleOrderMarkAsPaid}
onInvoiceRequest={(data: InvoiceRequest) => {
if (
data.invoiceRequest.errors.some(
err => err.code === InvoiceErrorCode.NO_INVOICE_PLUGIN
)
) {
notify({
title: intl.formatMessage({
defaultMessage: "Could not generate invoice",
description: "snackbar title"
}),
text: intl.formatMessage({
defaultMessage: "No invoice plugin installed",
description: "error message"
}),
status: "error"
});
}
if (
data.invoiceRequest.invoice.status === JobStatusEnum.SUCCESS
) {
orderMessages.handleInvoiceGenerateFinished(data);
} else {
orderMessages.handleInvoiceGeneratePending(data);
queue(Task.INVOICE_GENERATE, {
generateInvoice: {
invoiceId: data.invoiceRequest.invoice.id,
orderId: id
}
});
}
}}
onInvoiceSend={orderMessages.handleInvoiceSend}
>
{({
orderAddNote,
orderCancel,
orderDraftUpdate,
orderLinesAdd,
orderLineDelete,
orderLineUpdate,
orderPaymentCapture,
orderVoid,
orderShippingMethodUpdate,
orderUpdate,
orderFulfillmentApprove,
orderFulfillmentCancel,
orderFulfillmentUpdateTracking,
orderDraftCancel,
orderDraftFinalize,
orderPaymentMarkAsPaid,
orderInvoiceRequest,
orderInvoiceSend
}) => (
<>
{!isOrderDraft && !isOrderUnconfirmed && (
<OrderNormalDetails
id={id}
params={params}
data={data}
orderAddNote={orderAddNote}
orderInvoiceRequest={orderInvoiceRequest}
handleSubmit={handleSubmit}
orderCancel={orderCancel}
orderPaymentMarkAsPaid={orderPaymentMarkAsPaid}
orderVoid={orderVoid}
orderPaymentCapture={orderPaymentCapture}
orderFulfillmentApprove={orderFulfillmentApprove}
orderFulfillmentCancel={orderFulfillmentCancel}
orderFulfillmentUpdateTracking={
orderFulfillmentUpdateTracking
}
orderInvoiceSend={orderInvoiceSend}
updateMetadataOpts={updateMetadataOpts}
updatePrivateMetadataOpts={updatePrivateMetadataOpts}
openModal={openModal}
closeModal={closeModal}
/>
)}
{isOrderDraft && (
<OrderDraftDetails
id={id}
params={params}
loading={loading}
data={data}
orderAddNote={orderAddNote}
orderLineUpdate={orderLineUpdate}
orderLineDelete={orderLineDelete}
orderShippingMethodUpdate={orderShippingMethodUpdate}
orderLinesAdd={orderLinesAdd}
orderDraftUpdate={orderDraftUpdate}
orderDraftCancel={orderDraftCancel}
orderDraftFinalize={orderDraftFinalize}
openModal={openModal}
closeModal={closeModal}
/>
)}
{isOrderUnconfirmed && (
<OrderUnconfirmedDetails
id={id}
params={params}
data={data}
orderAddNote={orderAddNote}
orderLineUpdate={orderLineUpdate}
orderLineDelete={orderLineDelete}
orderInvoiceRequest={orderInvoiceRequest}
handleSubmit={handleSubmit}
orderCancel={orderCancel}
orderShippingMethodUpdate={orderShippingMethodUpdate}
orderLinesAdd={orderLinesAdd}
orderPaymentMarkAsPaid={orderPaymentMarkAsPaid}
orderVoid={orderVoid}
orderPaymentCapture={orderPaymentCapture}
orderFulfillmentApprove={orderFulfillmentApprove}
orderFulfillmentCancel={orderFulfillmentCancel}
orderFulfillmentUpdateTracking={
orderFulfillmentUpdateTracking
}
orderInvoiceSend={orderInvoiceSend}
updateMetadataOpts={updateMetadataOpts}
updatePrivateMetadataOpts={updatePrivateMetadataOpts}
openModal={openModal}
closeModal={closeModal}
/>
)}
<OrderAddressFields
isDraft={order?.status === OrderStatus.DRAFT}
orderUpdate={orderUpdate}
orderDraftUpdate={orderDraftUpdate}
data={data}
id={id}
onClose={closeModal}
action={params.action}
/>
</>
)}
</OrderOperations>
)}
</OrderDetailsMessages>
);
}}
</TypedOrderDetailsQuery>
);
};
export default OrderDetails;

View file

@ -1,6 +1,7 @@
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { extractMutationErrors } from "@saleor/misc";
import OrderFulfillPage from "@saleor/orders/components/OrderFulfillPage"; import OrderFulfillPage from "@saleor/orders/components/OrderFulfillPage";
import { useOrderFulfill } from "@saleor/orders/mutations"; import { useOrderFulfill } from "@saleor/orders/mutations";
import { import {
@ -100,6 +101,7 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
errors={fulfillOrderOpts.data?.orderFulfill.errors} errors={fulfillOrderOpts.data?.orderFulfill.errors}
onBack={() => navigate(orderUrl(orderId))} onBack={() => navigate(orderUrl(orderId))}
onSubmit={formData => onSubmit={formData =>
extractMutationErrors(
fulfillOrder({ fulfillOrder({
variables: { variables: {
input: { input: {
@ -113,6 +115,7 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
orderId orderId
} }
}) })
)
} }
order={data?.order} order={data?.order}
saveButtonBar="default" saveButtonBar="default"

View file

@ -1,5 +1,6 @@
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { extractMutationErrors } from "@saleor/misc";
import OrderRefundPage from "@saleor/orders/components/OrderRefundPage"; import OrderRefundPage from "@saleor/orders/components/OrderRefundPage";
import { import {
OrderRefundAmountCalculationMode, OrderRefundAmountCalculationMode,
@ -101,14 +102,14 @@ const OrderRefund: React.FC<OrderRefundProps> = ({ orderId }) => {
const handleSubmitMiscellaneousRefund = async ( const handleSubmitMiscellaneousRefund = async (
formData: OrderRefundSubmitData formData: OrderRefundSubmitData
) => { ) => {
const response = await refundOrder({ extractMutationErrors(
refundOrder({
variables: { variables: {
amount: formData.amount, amount: formData.amount,
id: orderId id: orderId
} }
}); })
);
return response?.errors || [];
}; };
const handleSubmitProductsRefund = async ( const handleSubmitProductsRefund = async (
@ -120,14 +121,14 @@ const OrderRefund: React.FC<OrderRefundProps> = ({ orderId }) => {
? getAutomaticallyCalculatedProductsRefundInput(formData) ? getAutomaticallyCalculatedProductsRefundInput(formData)
: getManuallySetProductsRefundInput(formData); : getManuallySetProductsRefundInput(formData);
const response = await refundOrderFulfillmentProducts({ return extractMutationErrors(
refundOrderFulfillmentProducts({
variables: { variables: {
input, input,
order: orderId order: orderId
} }
}); })
);
return response?.errors || [];
}; };
const handleSubmit = async (formData: OrderRefundSubmitData) => const handleSubmit = async (formData: OrderRefundSubmitData) =>

View file

@ -1,11 +1,11 @@
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import OrderReturnPage from "@saleor/orders/components/OrderReturnPage"; import OrderReturnPage from "@saleor/orders/components/OrderReturnPage";
import { OrderReturnFormData } from "@saleor/orders/components/OrderReturnPage/form"; import { OrderReturnFormData } from "@saleor/orders/components/OrderReturnPage/form";
import { useOrderReturnCreateMutation } from "@saleor/orders/mutations"; import { useOrderReturnCreateMutation } from "@saleor/orders/mutations";
import { useOrderQuery } from "@saleor/orders/queries"; import { useOrderQuery } from "@saleor/orders/queries";
import { FulfillmentReturnProducts_orderFulfillmentReturnProducts } from "@saleor/orders/types/FulfillmentReturnProducts";
import { orderUrl } from "@saleor/orders/urls"; import { orderUrl } from "@saleor/orders/urls";
import { OrderErrorCode } from "@saleor/types/globalTypes"; import { OrderErrorCode } from "@saleor/types/globalTypes";
import React from "react"; import React from "react";
@ -85,20 +85,14 @@ const OrderReturn: React.FC<OrderReturnProps> = ({ orderId }) => {
return; return;
} }
const result = await returnCreate({ return extractMutationErrors(
returnCreate({
variables: { variables: {
id: data.order.id, id: data.order.id,
input: new ReturnFormDataParser(data.order, formData).getParsedData() input: new ReturnFormDataParser(data.order, formData).getParsedData()
} }
}); })
);
const {
data: {
orderFulfillmentReturnProducts = {} as FulfillmentReturnProducts_orderFulfillmentReturnProducts
} = {}
} = result || {};
return orderFulfillmentReturnProducts.errors;
}; };
const navigateToOrder = (id?: string) => navigate(orderUrl(id || orderId)); const navigateToOrder = (id?: string) => navigate(orderUrl(id || orderId));

View file

@ -1,34 +1,53 @@
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getMutationState } from "@saleor/misc"; import { extractMutationErrors, getMutationState } from "@saleor/misc";
import OrderSettingsPage from "@saleor/orders/components/OrderSettingsPage"; import OrderSettingsPage from "@saleor/orders/components/OrderSettingsPage";
import { OrderSettingsFormData } from "@saleor/orders/components/OrderSettingsPage/form";
import { useOrderSettingsUpdateMutation } from "@saleor/orders/mutations"; import { useOrderSettingsUpdateMutation } from "@saleor/orders/mutations";
import { useOrderSettingsQuery } from "@saleor/orders/queries"; import { useOrderSettingsQuery } from "@saleor/orders/queries";
import { orderListUrl } from "@saleor/orders/urls"; import { orderListUrl } from "@saleor/orders/urls";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { OrderSettingsFormData } from "../components/OrderSettingsPage/types";
export const OrderSettings: React.FC = () => { export const OrderSettings: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const { data, loading } = useOrderSettingsQuery(); const { data, loading } = useOrderSettingsQuery({});
const [ const [
orderSettingsUpdate, orderSettingsUpdate,
orderSettingsUpdateOpts orderSettingsUpdateOpts
] = useOrderSettingsUpdateMutation({}); ] = useOrderSettingsUpdateMutation({
onCompleted: ({ orderSettingsUpdate: { errors } }) => {
if (!errors.length) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
return;
}
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
});
const handleBack = () => navigate(orderListUrl());
const handleSubmit = async ({ const handleSubmit = async ({
automaticallyConfirmAllNewOrders, automaticallyConfirmAllNewOrders,
automaticallyFulfillNonShippableGiftCard, automaticallyFulfillNonShippableGiftCard,
fulfillmentAutoApprove, fulfillmentAutoApprove,
fulfillmentAllowUnpaid fulfillmentAllowUnpaid
}: OrderSettingsFormData) => { }: OrderSettingsFormData) =>
const result = await orderSettingsUpdate({ extractMutationErrors(
orderSettingsUpdate({
variables: { variables: {
orderSettingsInput: { orderSettingsInput: {
automaticallyFulfillNonShippableGiftCard, automaticallyFulfillNonShippableGiftCard,
@ -39,25 +58,8 @@ export const OrderSettings: React.FC = () => {
fulfillmentAllowUnpaid fulfillmentAllowUnpaid
} }
} }
}); })
);
const errors = result.data?.orderSettingsUpdate.errors;
if (errors.length) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
return errors;
}
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
return [];
};
const handleBack = () => navigate(orderListUrl());
return ( return (
<OrderSettingsPage <OrderSettingsPage

View file

@ -55,7 +55,7 @@ const PageTypeCreatePage: React.FC<PageTypeCreatePageProps> = props => {
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
return ( return (
<Form initial={formInitialData} onSubmit={onSubmit} confirmLeave> <Form confirmLeave initial={formInitialData} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);

View file

@ -106,7 +106,7 @@ const PageTypeDetailsPage: React.FC<PageTypeDetailsPageProps> = props => {
}; };
return ( return (
<Form initial={formInitialData} onSubmit={handleSubmit} confirmLeave> <Form confirmLeave initial={formInitialData} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);

View file

@ -1,6 +1,7 @@
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
useMetadataUpdate, useMetadataUpdate,
@ -46,8 +47,12 @@ export const PageTypeCreate: React.FC = () => {
} }
}); });
return result.data?.pageTypeCreate.pageType?.id || null; return {
id: result.data?.pageTypeCreate.pageType?.id || null,
errors: getMutationErrors(result)
}; };
};
const handleSubmit = createMetadataCreateHandler( const handleSubmit = createMetadataCreateHandler(
handleCreate, handleCreate,
updateMetadata, updateMetadata,

View file

@ -10,13 +10,19 @@ import {
createFetchReferencesHandler createFetchReferencesHandler
} from "@saleor/attributes/utils/handlers"; } from "@saleor/attributes/utils/handlers";
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useForm, {
CommonUseFormResultWithHandlers,
FormChange,
SubmitPromise
} from "@saleor/hooks/useForm";
import useFormset, { import useFormset, {
FormsetChange, FormsetChange,
FormsetData FormsetData
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { import {
PageDetails_page, PageDetails_page,
PageDetails_page_pageType PageDetails_page_pageType
@ -32,12 +38,11 @@ import { SearchPageTypes_search_edges_node } from "@saleor/searches/types/Search
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import { FetchMoreProps, ReorderEvent } from "@saleor/types"; import { FetchMoreProps, ReorderEvent } from "@saleor/types";
import getPublicationData from "@saleor/utils/data/getPublicationData"; import getPublicationData from "@saleor/utils/data/getPublicationData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React, { useEffect } from "react";
export interface PageFormData extends MetadataFormData { export interface PageFormData extends MetadataFormData {
isPublished: boolean; isPublished: boolean;
@ -71,13 +76,10 @@ export interface PageUpdateHandlers {
fetchReferences: (value: string) => void; fetchReferences: (value: string) => void;
fetchMoreReferences: FetchMoreProps; fetchMoreReferences: FetchMoreProps;
} }
export interface UsePageUpdateFormResult {
change: FormChange; export interface UsePageUpdateFormResult
data: PageData; extends CommonUseFormResultWithHandlers<PageData, PageUpdateHandlers> {
valid: boolean; valid: boolean;
handlers: PageUpdateHandlers;
hasChanged: boolean;
submit: () => void;
} }
export interface UsePageFormOpts { export interface UsePageFormOpts {
@ -99,14 +101,23 @@ export interface PageFormProps extends UsePageFormOpts {
onSubmit: (data: PageData) => SubmitPromise; onSubmit: (data: PageData) => SubmitPromise;
} }
const getInitialFormData = (page?: PageDetails_page): PageFormData => ({
isPublished: page?.isPublished,
metadata: page?.metadata?.map(mapMetadataItemToInput) || [],
pageType: null,
privateMetadata: page?.privateMetadata?.map(mapMetadataItemToInput) || [],
publicationDate: page?.publicationDate || "",
seoDescription: page?.seoDescription || "",
seoTitle: page?.seoTitle || "",
slug: page?.slug || "",
title: page?.title || ""
});
function usePageForm( function usePageForm(
page: PageDetails_page, page: PageDetails_page,
onSubmit: (data: PageData) => SubmitPromise, onSubmit: (data: PageData) => SubmitPromise,
opts: UsePageFormOpts opts: UsePageFormOpts
): UsePageUpdateFormResult { ): UsePageUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const pageExists = page !== null; const pageExists = page !== null;
const attributes = useFormset( const attributes = useFormset(
@ -118,19 +129,21 @@ function usePageForm(
); );
const attributesWithNewFileValue = useFormset<null, File>([]); const attributesWithNewFileValue = useFormset<null, File>([]);
const form = useForm<PageFormData>({ const {
isPublished: page?.isPublished, handleChange,
metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], triggerChange,
pageType: null, setChanged,
privateMetadata: pageExists hasChanged,
? page?.privateMetadata?.map(mapMetadataItemToInput) data: formData,
: [], formId
publicationDate: page?.publicationDate || "", } = useForm(getInitialFormData(page), undefined, {
seoDescription: page?.seoDescription || "", confirmLeave: true
seoTitle: page?.seoTitle || "",
slug: page?.slug || "",
title: page?.title || ""
}); });
const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const [content, changeContent] = useRichText({ const [content, changeContent] = useRichText({
initial: pageExists ? page?.content : null, initial: pageExists ? page?.content : null,
triggerChange triggerChange
@ -142,10 +155,6 @@ function usePageForm(
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
const handlePageTypeSelect = createPageTypeSelectHandler( const handlePageTypeSelect = createPageTypeSelectHandler(
opts.onSelectPageType, opts.onSelectPageType,
@ -191,7 +200,7 @@ function usePageForm(
// Need to make it function to always have content.current up to date // Need to make it function to always have content.current up to date
const getData = (): PageData => ({ const getData = (): PageData => ({
...form.data, ...formData,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -204,8 +213,8 @@ function usePageForm(
const getSubmitData = (): PageSubmitData => ({ const getSubmitData = (): PageSubmitData => ({
...getData(), ...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), ...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
...getPublicationData(form.data), ...getPublicationData(formData),
attributesWithNewFileValue: attributesWithNewFileValue.data attributesWithNewFileValue: attributesWithNewFileValue.data
}); });
@ -219,10 +228,15 @@ function usePageForm(
return errors; return errors;
}; };
const submit = () => const handleFormSubmit = useHandleFormSubmit({
pageExists formId,
? handleFormSubmit(getSubmitData(), handleSubmit, setChanged) onSubmit: handleSubmit,
: onSubmit(getSubmitData()); setChanged
});
const submit = () => handleFormSubmit(getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const valid = pageExists || !!opts.selectedPageType; const valid = pageExists || !!opts.selectedPageType;
@ -242,7 +256,7 @@ function usePageForm(
selectAttributeReference: handleAttributeReferenceChange, selectAttributeReference: handleAttributeReferenceChange,
selectPageType: handlePageTypeSelect selectPageType: handlePageTypeSelect
}, },
hasChanged: changed, hasChanged,
submit submit
}; };
} }

View file

@ -12,6 +12,7 @@ import {
import { useFileUploadMutation } from "@saleor/files/mutations"; import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import usePageSearch from "@saleor/searches/usePageSearch"; import usePageSearch from "@saleor/searches/usePageSearch";
import usePageTypeSearch from "@saleor/searches/usePageTypeSearch"; import usePageTypeSearch from "@saleor/searches/usePageTypeSearch";
import useProductSearch from "@saleor/searches/useProductSearch"; import useProductSearch from "@saleor/searches/useProductSearch";
@ -170,8 +171,12 @@ export const PageCreate: React.FC<PageCreateProps> = ({ params }) => {
} }
}); });
return result.data.pageCreate.page?.id || null; return {
id: result.data.pageCreate.page?.id || null,
errors: getMutationErrors(result)
}; };
};
const handleSubmit = createMetadataCreateHandler( const handleSubmit = createMetadataCreateHandler(
handleCreate, handleCreate,
updateMetadata, updateMetadata,

View file

@ -4,6 +4,7 @@ import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { PermissionGroupErrorFragment } from "@saleor/fragments/types/PermissionGroupErrorFragment"; import { PermissionGroupErrorFragment } from "@saleor/fragments/types/PermissionGroupErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
@ -16,14 +17,14 @@ import { useIntl } from "react-intl";
import { PermissionData } from "../PermissionGroupDetailsPage"; import { PermissionData } from "../PermissionGroupDetailsPage";
import PermissionGroupInfo from "../PermissionGroupInfo"; import PermissionGroupInfo from "../PermissionGroupInfo";
export interface PermissionGroupCreatePageFormData { export interface PermissionGroupCreateFormData {
name: string; name: string;
hasFullAccess: boolean; hasFullAccess: boolean;
isActive: boolean; isActive: boolean;
permissions: PermissionEnum[]; permissions: PermissionEnum[];
} }
const initialForm: PermissionGroupCreatePageFormData = { const initialForm: PermissionGroupCreateFormData = {
hasFullAccess: false, hasFullAccess: false,
isActive: false, isActive: false,
name: "", name: "",
@ -36,7 +37,7 @@ export interface PermissionGroupCreatePageProps {
permissions: PermissionData[]; permissions: PermissionData[];
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit(data: PermissionGroupCreatePageFormData); onSubmit: (data: PermissionGroupCreateFormData) => SubmitPromise;
} }
const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
@ -56,7 +57,7 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
); );
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ data, change, submit, hasChanged }) => ( {({ data, change, submit, hasChanged }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>

Some files were not shown because too many files have changed in this diff Show more