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",
"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": {
"context": "image upload",
"string": "Drop here to upload"
@ -5382,20 +5398,12 @@
"src_dot_orders_dot_views_dot_OrderDetails_dot_4085755992": {
"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": {
"string": "Invoice is Generating"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_617145655": {
"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": {
"string": "Fulfillment successfully cancelled"
},
@ -8371,6 +8379,18 @@
"context": "section header",
"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": {
"string": "No webhooks found"
},

View file

@ -1,11 +1,13 @@
import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg";
import plusIcon from "@assets/images/plus-icon.svg";
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 CardTitle from "@saleor/components/CardTitle";
import Container from "@saleor/components/Container";
import Hr from "@saleor/components/Hr";
import Skeleton from "@saleor/components/Skeleton";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { buttonMessages } from "@saleor/intl";
import { Button } from "@saleor/macaw-ui";
import classNames from "classnames";
@ -19,7 +21,7 @@ export interface AppInstallPageProps {
data: AppFetch_appFetchManifest_manifest;
loading: boolean;
navigateToAppsList: () => void;
onSubmit: () => void;
onSubmit: () => SubmitPromise<AppInstall_appInstall_errors[]>;
}
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 { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { AppErrorFragment } from "@saleor/fragments/types/AppErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui";
@ -28,7 +29,9 @@ export interface CustomAppCreatePageProps {
permissions: ShopInfo_shop_permissions[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: CustomAppCreatePageFormData) => void;
onSubmit: (
data: CustomAppCreatePageFormData
) => SubmitPromise<AppErrorFragment[]>;
}
const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
@ -52,7 +55,7 @@ const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
const permissionsError = getAppErrorMessage(formErrors.permissions, intl);
return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ data, change, hasChanged, submit }) => (
<Container>
<Backlink onClick={onBack}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { DEMO_MODE } from "@saleor/config";
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";
export interface LoginFormData {
@ -12,7 +12,7 @@ export interface UseLoginFormResult {
change: FormChange;
data: LoginFormData;
hasChanged: boolean;
submit: () => Promise<boolean>;
submit: () => SubmitPromise;
}
export interface LoginFormProps {
@ -33,32 +33,18 @@ const getLoginFormData = () => {
function useLoginForm(
onSubmit: (data: LoginFormData) => SubmitPromise
): UseLoginFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm(getLoginFormData());
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const { change, hasChanged, data, setChanged } = form;
const data: LoginFormData = {
...form.data
};
const handleFormSubmit = useHandleFormSubmit({ onSubmit, setChanged });
const handleSubmit = async (data: LoginFormData) => {
const errors = await onSubmit(data);
return errors;
};
const submit = async () => handleFormSubmit(data, handleSubmit, setChanged);
const submit = async () => handleFormSubmit(data);
return {
change: handleChange,
change,
data,
hasChanged: changed,
hasChanged,
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 Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { Button } from "@saleor/macaw-ui";
import { SetPasswordData } from "@saleor/sdk";
import getAccountErrorMessage from "@saleor/utils/errors/account";
@ -16,7 +17,7 @@ export interface NewPasswordPageFormData {
export interface NewPasswordPageProps {
disabled: boolean;
errors: SetPasswordData["errors"];
onSubmit: (data: NewPasswordPageFormData) => void;
onSubmit: (data: NewPasswordPageFormData) => SubmitPromise;
}
const initialForm: NewPasswordPageFormData = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import {
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(
handleCreate,
updateMetadata,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -59,18 +59,20 @@ export const EditableTableCell: React.FC<EditableTableCellProps> = props => {
defaultValue,
focused,
InputProps,
value,
onConfirm
value
// onConfirm
} = props;
const classes = useStyles(props);
const handleConfirm = (data: { value: string }) => {
disable();
onConfirm(data.value);
};
// const handleConfirm = (data: { value: string }) => {
// disable();
// onConfirm(data.value);
// };
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 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 React from "react";
export interface FormProps<T>
import { FormId } from "./ExitFormDialogProvider";
export interface FormProps<TData, TErrors>
extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit"> {
children: (props: UseFormResult<T>) => React.ReactNode;
children: (props: UseFormResult<TData>) => React.ReactNode;
confirmLeave?: boolean;
initial?: T;
initial?: TData;
resetOnSubmit?: boolean;
onSubmit?: (data: T) => SubmitPromise | void;
onSubmit?: (data: TData) => SubmitPromise<TErrors[]> | void;
formId?: FormId;
}
function Form<T>(props: FormProps<T>) {
const { children, initial, resetOnSubmit, onSubmit, ...rest } = props;
const renderProps = useForm(initial, onSubmit);
function Form<TData, Terrors>({
children,
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) {
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 { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment";
import useAddressValidation from "@saleor/hooks/useAddressValidation";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { Backlink } from "@saleor/macaw-ui";
import { extractMutationErrors } from "@saleor/misc";
import { AddressInput } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { mapCountriesToChoices } from "@saleor/utils/maps";
@ -56,7 +58,7 @@ export interface CustomerCreatePageProps {
errors: AccountErrorFragment[];
saveButtonBar: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: CustomerCreatePageSubmitData) => void;
onSubmit: (data: CustomerCreatePageSubmitData) => SubmitPromise;
}
const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
@ -117,20 +119,22 @@ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
.some(field => field !== "");
if (areAddressInputFieldsModified) {
handleSubmitWithAddress(formData);
} else {
return handleSubmitWithAddress(formData);
}
return extractMutationErrors(
onSubmit({
address: null,
customerFirstName: formData.customerFirstName,
customerLastName: formData.customerLastName,
email: formData.email,
note: formData.note
});
}
})
);
};
return (
<Form initial={initialForm} onSubmit={handleSubmit} confirmLeave>
<Form confirmLeave initial={initialForm} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => {
const handleCountrySelect = createSingleAutocompleteSelectHandler(
change,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,16 @@ import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCar
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
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 Savebar from "@saleor/components/Savebar";
import {
createChannelsChangeHandler,
createDiscountTypeChangeHandler
} from "@saleor/discounts/handlers";
import { VOUCHER_CREATE_FORM_ID } from "@saleor/discounts/views/VoucherCreate/types";
import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } 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 { DiscountTypeEnum, RequirementsPicker } from "../../types";
import VoucherDates from "../VoucherDates";
import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
import VoucherInfo from "../VoucherInfo";
import VoucherLimits from "../VoucherLimits";
import VoucherRequirements from "../VoucherRequirements";
import VoucherTypes from "../VoucherTypes";
import VoucherValue from "../VoucherValue";
export interface FormData extends MetadataFormData {
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;
export interface FormData extends VoucherDetailsPageFormData {
value: number;
}
@ -60,7 +46,7 @@ export interface VoucherCreatePageProps {
onBack: () => void;
onChannelsChange: (data: ChannelVoucherData[]) => void;
openChannelsModal: () => void;
onSubmit: (data: FormData) => void;
onSubmit: (data: FormData) => SubmitPromise;
}
const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
@ -104,7 +90,12 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
};
return (
<Form initial={initialForm} onSubmit={onSubmit}>
<Form
confirmLeave
initial={initialForm}
onSubmit={onSubmit}
formId={VOUCHER_CREATE_FORM_ID}
>
{({ change, data, hasChanged, submit, triggerChange, set }) => {
const handleDiscountTypeChange = createDiscountTypeChangeHandler(
change

View file

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

View file

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

View file

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

View file

@ -8,7 +8,11 @@ import {
SaleCreate,
SaleCreateVariables
} from "@saleor/discounts/types/SaleCreate";
import { joinDateTime } from "@saleor/misc";
import {
extractMutationErrors,
getMutationErrors,
joinDateTime
} from "@saleor/misc";
import { decimal } from "@saleor/misc";
import { DiscountValueTypeEnum, SaleType } from "@saleor/types/globalTypes";
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({
variables: getSaleChannelsVariables(
response.data.saleCreate.sale.id,
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 { messages } from "./messages";
import { SALE_UPDATE_FORM_ID } from "./types";
interface SaleDetailsProps {
id: string;
@ -144,10 +145,15 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
isChannelsModalOpen,
setCurrentChannels,
toggleAllChannels
} = useChannels(saleChannelsChoices, params?.action, {
} = useChannels(
saleChannelsChoices,
params?.action,
{
closeModal,
openModal
});
},
{ formId: SALE_UPDATE_FORM_ID }
);
const [selectedChannel] = useLocalStorage("salesListChannel", "");

View file

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

View file

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

View file

@ -9,7 +9,11 @@ import {
VoucherCreate,
VoucherCreateVariables
} from "@saleor/discounts/types/VoucherCreate";
import { joinDateTime } from "@saleor/misc";
import {
extractMutationErrors,
getMutationErrors,
joinDateTime
} from "@saleor/misc";
import {
DiscountValueTypeEnum,
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({
variables: getChannelsVariables(
response.data.voucherCreate.voucher.id,
formData,
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 { productUrl } from "../../../products/urls";
import { createUpdateHandler } from "./handlers";
import { VOUCHER_UPDATE_FORM_ID } from "./types";
interface VoucherDetailsProps {
id: string;
@ -152,10 +153,15 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
isChannelsModalOpen,
setCurrentChannels,
toggleAllChannels
} = useChannels(voucherChannelsChoices, params?.action, {
} = useChannels(
voucherChannelsChoices,
params?.action,
{
closeModal,
openModal
});
},
{ formId: VOUCHER_UPDATE_FORM_ID }
);
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 { MutationResultWithOpts } from "@saleor/hooks/makeMutation";
import useForm, { FormChange, UseFormResult } from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useNotifier from "@saleor/hooks/useNotifier";
import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils";
import { getFormErrors } from "@saleor/utils/errors";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
@ -131,9 +131,22 @@ const GiftCardUpdateFormProvider: React.FC<GiftCardUpdateFormProviderProps> = ({
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 {
isMetadataModified,
@ -148,15 +161,7 @@ const GiftCardUpdateFormProvider: React.FC<GiftCardUpdateFormProviderProps> = ({
...getMetadata(data, isMetadataModified, isPrivateMetadataModified)
};
const handleSubmit = createMetadataUpdateHandler(
giftCard,
submit,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const formSubmit = () =>
handleFormSubmit(submitData, handleSubmit, setChanged);
const formSubmit = () => handleFormSubmit(submitData);
const formErrors = getFormErrors(
["tags", "expiryDate"],

View file

@ -11,7 +11,9 @@ import { useState } from "react";
interface UseAddressValidation<TInput, TOutput> {
errors: AccountErrorFragment[];
submit: (data: TInput & AddressTypeInput) => TOutput;
submit: (
data: TInput & AddressTypeInput
) => TOutput | Promise<AccountErrorFragment[]>;
}
function useAddressValidation<TInput, TOutput>(
@ -42,7 +44,10 @@ function useAddressValidation<TInput, TOutput>(
);
return onSubmit(transformFormToAddressInput(data));
} 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", () => {
// Given
const { result } = renderHook(() =>
useChannels(channels, "", {
useChannels(
channels,
"",
{
closeModal: jest.fn,
openModal: jest.fn
})
},
{ formId: Symbol() }
)
);
// When
@ -42,10 +47,15 @@ describe("useChannels", () => {
it("properly removes channels", () => {
// Given
const { result } = renderHook(() =>
useChannels(channels, "", {
useChannels(
channels,
"",
{
closeModal: jest.fn,
openModal: jest.fn
})
},
{ formId: Symbol() }
)
);
// When
@ -68,10 +78,15 @@ describe("useChannels", () => {
it("doesn't not save changes if closed without confirm", () => {
// Given
const { result } = renderHook(() =>
useChannels(channels, "", {
useChannels(
channels,
"",
{
closeModal: jest.fn,
openModal: jest.fn
})
},
{ formId: Symbol() }
)
);
// When

View file

@ -1,5 +1,7 @@
import { ChannelsAction } from "@saleor/channels/urls";
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 useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -11,8 +13,15 @@ interface Modal {
function useChannels<T extends Channel, A>(
channels: T[],
action: A | ChannelsAction,
{ closeModal, openModal }: Modal
{ closeModal, openModal }: Modal,
opts: WithFormId
) {
const { formId } = opts;
const { setIsDirty } = useExitFormDialog({
formId
});
const [currentChannels, setCurrentChannels] = useStateFromProps(channels);
const {
@ -34,6 +43,11 @@ function useChannels<T extends Channel, A>(
(channel, nextChannel) => channel.name.localeCompare(nextChannel.name)
);
setCurrentChannels(sortedChannelListElements);
// hack so channels also update exit form dalog provider
// despite not setting page's form data "changed" prop
setIsDirty(true);
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 isEqual from "lodash/isEqual";
import omit from "lodash/omit";
import React from "react";
import React, { useEffect } from "react";
import { useState } from "react";
import useStateFromProps from "./useStateFromProps";
@ -12,7 +18,7 @@ export interface ChangeEvent<TData = any> {
value: TData;
};
}
export type SubmitPromise = Promise<any[]>;
export type SubmitPromise<TData = any> = Promise<TData>;
export type FormChange = (event: ChangeEvent, cb?: () => void) => void;
@ -20,19 +26,35 @@ export type FormErrors<T> = {
[field in keyof T]?: string | React.ReactNode;
};
export interface UseFormResult<T> {
change: FormChange;
data: T;
hasChanged: boolean;
export interface UseFormOpts {
confirmLeave: boolean;
formId?: FormId;
}
export interface UseFormResult<TData>
extends CommonUseFormResult<TData>,
Pick<UseExitFormDialogResult, "formId"> {
reset: () => void;
set: (data: Partial<T>) => void;
submit: () => void;
set: (data: Partial<TData>) => void;
triggerChange: () => void;
toggleValue: FormChange;
errors: FormErrors<T>;
setChanged: (value: boolean) => void;
setError: (name: keyof T, error: string | React.ReactNode) => void;
clearErrors: (name?: keyof T | Array<keyof T>) => void;
handleChange: FormChange;
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[]>;
@ -60,25 +82,61 @@ function handleRefresh<T extends FormData>(
}
}
function useForm<T extends FormData>(
initial: T,
onSubmit?: (data: T) => SubmitPromise | void
function useForm<T extends FormData, TErrors>(
initialData: T,
onSubmit?: (data: T) => SubmitPromise<TErrors[]> | void,
opts: UseFormOpts = { confirmLeave: false, formId: undefined }
): UseFormResult<T> {
const { confirmLeave, formId: propsFormId } = opts;
const [hasChanged, setChanged] = useState(false);
const [errors, setErrors] = useState<FormErrors<T>>({});
const [data, setData] = useStateFromProps(initial, {
const [data, setData] = useStateFromProps(initialData, {
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) {
const { name, value } = event.target;
const field = data[name as keyof T];
if (Array.isArray(field)) {
if (!hasChanged) {
setChanged(true);
handleSetChanged(true);
}
setData({
...data,
[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) {
const { name, value } = event.target;
@ -98,7 +161,7 @@ function useForm<T extends FormData>(
return;
} else {
if (data[name] !== value) {
setChanged(true);
handleSetChanged(true);
}
setData(data => ({
...data,
@ -108,7 +171,7 @@ function useForm<T extends FormData>(
}
function reset() {
setData(initial);
setData(initialData);
}
function set(newData: Partial<T>, setHasChanged = true) {
@ -116,23 +179,15 @@ function useForm<T extends FormData>(
...data,
...newData
}));
setChanged(setHasChanged);
handleSetChanged(setHasChanged);
}
async function submit() {
if (typeof onSubmit === "function" && !Object.keys(errors).length) {
const result = onSubmit(data);
if (result) {
const errors = await result;
if (errors?.length === 0) {
setChanged(false);
}
}
}
}
const result = handleFormSubmit(data);
function triggerChange() {
setChanged(true);
return result;
}
}
const setError = (field: keyof T, error: string | React.ReactNode) =>
@ -149,6 +204,7 @@ function useForm<T extends FormData>(
};
return {
formId,
setError,
errors,
change,
@ -156,11 +212,12 @@ function useForm<T extends FormData>(
data,
hasChanged,
reset,
setChanged,
set,
submit,
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";
export type UseNavigatorResult = (
@ -14,11 +16,18 @@ function useNavigator(): UseNavigatorResult {
history
} = useRouter();
const { shouldBlockNavigation } = useContext(ExitFormDialogContext);
return (
url: string,
{ replace = false, preserveQs = false, resetScroll = false } = {}
) => {
if (shouldBlockNavigation()) {
return;
}
const targetUrl = preserveQs ? url + search : url;
if (replace) {
history.replace(targetUrl);
} else {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import useForm, { FormChange } from "@saleor/hooks/useForm";
import React, { useState } from "react";
import useForm, { CommonUseFormResult } from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import React from "react";
export enum CustomerChangeActionEnum {
KEEP_ADDRESS = "keepAddress",
@ -10,12 +11,9 @@ export interface OrderCustomerChangeData {
changeActionOption: CustomerChangeActionEnum;
}
interface UseOrderCustomerChangeFormResult {
submit: (event: React.FormEvent<any>) => void;
change: FormChange;
hasChanged: boolean;
data: OrderCustomerChangeData;
}
type UseOrderCustomerChangeFormResult = CommonUseFormResult<
OrderCustomerChangeData
>;
export interface OrderCustomerChangeFormProps {
children: (props: UseOrderCustomerChangeFormResult) => React.ReactNode;
@ -23,38 +21,37 @@ export interface OrderCustomerChangeFormProps {
onSubmit: (data: OrderCustomerChangeData) => void;
}
function useOrderCustomerChangeForm(
initial: Partial<OrderCustomerChangeData>,
onSubmit: (data: OrderCustomerChangeData) => void
): UseOrderCustomerChangeFormResult {
const defaultInitialFormData: OrderCustomerChangeData = {
changeActionOption: CustomerChangeActionEnum.KEEP_ADDRESS
};
const form = useForm({
function useOrderCustomerChangeForm(
initial: Partial<OrderCustomerChangeData> = {},
onSubmit: (data: OrderCustomerChangeData) => void
): UseOrderCustomerChangeFormResult {
const { handleChange, hasChanged, data, setChanged } = useForm({
...initial,
...defaultInitialFormData
});
const [changed, setChanged] = useState(false);
const triggerChange = () => setChanged(true);
const handleFormSubmit = useHandleFormSubmit({
onSubmit,
setChanged
});
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const handleSubmit = () => handleFormSubmit(data);
const submit = (event: React.FormEvent<any>) => {
event.stopPropagation();
event.preventDefault();
return onSubmit(form.data);
return handleSubmit();
};
return {
change: handleChange,
submit,
hasChanged: changed,
data: form.data
hasChanged,
data
};
}
@ -63,7 +60,7 @@ const OrderCustomerChangeForm: React.FC<OrderCustomerChangeFormProps> = ({
initial,
onSubmit
}) => {
const props = useOrderCustomerChangeForm(initial || {}, onSubmit);
const props = useOrderCustomerChangeForm(initial, onSubmit);
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 { defineMessages, useIntl } from "react-intl";
import { maybe } from "../../../misc";
import { getMutationErrors, maybe } from "../../../misc";
import { OrderStatus } from "../../../types/globalTypes";
import {
OrderDetails_order,
@ -173,7 +173,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
privateMetadata
});
resetMetadataChanged();
return result;
return getMutationErrors(result);
};
const initial: MetadataFormData = {
@ -212,7 +212,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
]);
return (
<Form initial={initial} onSubmit={handleSubmit}>
<Form confirmLeave initial={initial} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import {
TimelineEventProps,
TimelineNote
} from "@saleor/components/Timeline";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { makeStyles } from "@saleor/macaw-ui";
import { OrderDetails_order_events } from "@saleor/orders/types/OrderDetails";
import {
@ -295,7 +296,7 @@ const useStyles = makeStyles(
interface OrderHistoryProps {
history: OrderDetails_order_events[];
orderCurrency: string;
onNoteAdd: (data: FormData) => void;
onNoteAdd: (data: FormData) => SubmitPromise;
}
const OrderHistory: React.FC<OrderHistoryProps> = props => {
@ -331,7 +332,12 @@ const OrderHistory: React.FC<OrderHistoryProps> = props => {
<Hr />
{history ? (
<Timeline>
<Form initial={{ message: "" }} onSubmit={onNoteAdd} resetOnSubmit>
<Form
confirmLeave
initial={{ message: "" }}
onSubmit={onNoteAdd}
resetOnSubmit
>
{({ change, data, reset, submit }) => (
<TimelineAddNote
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, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { OrderRefundData_order } from "@saleor/orders/types/OrderRefundData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import React from "react";
import React, { useEffect } from "react";
import { refundFulfilledStatuses } from "./OrderRefundPage";
@ -40,13 +44,12 @@ export interface OrderRefundFormData extends OrderRefundData {
export type OrderRefundSubmitData = OrderRefundFormData;
export interface UseOrderRefundFormResult {
change: FormChange;
data: OrderRefundFormData;
export interface UseOrderRefundFormResult
extends CommonUseFormResultWithHandlers<
OrderRefundFormData,
OrderRefundHandlers
> {
disabled: boolean;
handlers: OrderRefundHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
interface OrderRefundFormProps {
@ -72,10 +75,19 @@ function useOrderRefundForm(
defaultType: OrderRefundType,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise
): UseOrderRefundFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const {
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>(
order?.lines
.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> = (
id,
value
@ -165,12 +173,20 @@ function useOrderRefundForm(
};
const data: OrderRefundFormData = {
...form.data,
...formData,
refundedFulfilledProductQuantities: refundedFulfilledProductQuantities.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;
@ -184,7 +200,7 @@ function useOrderRefundForm(
setMaximalRefundedFulfilledProductQuantities: handleMaximalRefundedFulfilledProductQuantitiesSet,
setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet
},
hasChanged: changed,
hasChanged,
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, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import { FulfillmentStatus } from "@saleor/types/globalTypes";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import React, { useState } from "react";
import React, { useEffect } from "react";
import { OrderRefundAmountCalculationMode } from "../OrderRefundPage/form";
import {
@ -55,13 +59,10 @@ export interface OrderReturnFormData extends OrderReturnData {
export type OrderRefundSubmitData = OrderReturnFormData;
export interface UseOrderRefundFormResult {
change: FormChange;
hasChanged: boolean;
data: OrderReturnFormData;
handlers: OrderReturnHandlers;
submit: () => Promise<boolean>;
}
export type UseOrderRefundFormResult = CommonUseFormResultWithHandlers<
OrderReturnFormData,
OrderReturnHandlers
>;
interface OrderReturnProps {
children: (props: UseOrderRefundFormResult) => React.ReactNode;
@ -79,12 +80,18 @@ function useOrderReturnForm(
order: OrderDetails_order,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise
): UseOrderRefundFormResult {
const form = useForm(getOrderRefundPageFormData());
const [hasChanged, setHasChanged] = useState(false);
const {
handleChange,
setChanged,
hasChanged,
data: formData,
triggerChange,
formId
} = useForm(getOrderRefundPageFormData(), undefined, {
confirmLeave: true
});
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
};
const { setExitDialogSubmitRef } = useExitFormDialog();
const unfulfiledItemsQuantites = useFormset<LineItemData, number>(
getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 }))
@ -215,12 +222,18 @@ function useOrderReturnForm(
waitingItemsQuantities: waitingItemsQuantities.data,
itemsToBeReplaced: itemsToBeReplaced.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) {
return (id: string, value: T) => {

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment";
import { ShopOrderSettingsFragment } from "@saleor/fragments/types/ShopOrderSettingsFragment";
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";
export interface OrderSettingsFormData {
@ -15,7 +15,7 @@ export interface UseOrderSettingsFormResult {
change: FormChange;
data: OrderSettingsFormData;
hasChanged: boolean;
submit: () => Promise<boolean>;
submit: () => SubmitPromise<any[]>;
}
export interface OrderSettingsFormProps {
@ -45,20 +45,20 @@ function useOrderSettingsForm(
onSubmit: (data: OrderSettingsFormData) => SubmitPromise
): UseOrderSettingsFormResult {
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) => {
form.change(event, cb);
triggerChange();
};
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
setChanged
});
const data: OrderSettingsFormData = {
...form.data
};
const submit = () => handleFormSubmit(form.data, onSubmit, setChanged);
const submit = () => handleFormSubmit(data);
return {
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 { customerUrl } from "../../../../customers/urls";
import { getStringOrPlaceholder } from "../../../../misc";
import {
extractMutationErrors,
getStringOrPlaceholder
} from "../../../../misc";
import { productUrl } from "../../../../products/urls";
import OrderDraftCancelDialog from "../../../components/OrderDraftCancelDialog/OrderDraftCancelDialog";
import OrderDraftPage from "../../../components/OrderDraftPage";
@ -172,10 +175,12 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
<OrderDraftPage
disabled={loading}
onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({
input: variables,
order: id
})
)
}
users={mapEdgesToItems(users?.data?.search)}
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
@ -245,6 +250,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
onFetch={variantSearch}
onFetchMore={loadMore}
onSubmit={variants =>
extractMutationErrors(
orderLinesAdd.mutate({
id,
input: variants.map(variant => ({
@ -252,6 +258,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
variantId: variant.id
}))
})
)
}
/>
<OrderCustomerChangeDialog

View file

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

View file

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

View file

@ -1,289 +1,2 @@
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 {
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;
export * from "./OrderDetails";
export { default } from "./OrderDetails";

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import OrderReturnPage from "@saleor/orders/components/OrderReturnPage";
import { OrderReturnFormData } from "@saleor/orders/components/OrderReturnPage/form";
import { useOrderReturnCreateMutation } from "@saleor/orders/mutations";
import { useOrderQuery } from "@saleor/orders/queries";
import { FulfillmentReturnProducts_orderFulfillmentReturnProducts } from "@saleor/orders/types/FulfillmentReturnProducts";
import { orderUrl } from "@saleor/orders/urls";
import { OrderErrorCode } from "@saleor/types/globalTypes";
import React from "react";
@ -85,20 +85,14 @@ const OrderReturn: React.FC<OrderReturnProps> = ({ orderId }) => {
return;
}
const result = await returnCreate({
return extractMutationErrors(
returnCreate({
variables: {
id: data.order.id,
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));

View file

@ -1,34 +1,53 @@
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { getMutationState } from "@saleor/misc";
import { extractMutationErrors, getMutationState } from "@saleor/misc";
import OrderSettingsPage from "@saleor/orders/components/OrderSettingsPage";
import { OrderSettingsFormData } from "@saleor/orders/components/OrderSettingsPage/form";
import { useOrderSettingsUpdateMutation } from "@saleor/orders/mutations";
import { useOrderSettingsQuery } from "@saleor/orders/queries";
import { orderListUrl } from "@saleor/orders/urls";
import React from "react";
import { useIntl } from "react-intl";
import { OrderSettingsFormData } from "../components/OrderSettingsPage/types";
export const OrderSettings: React.FC = () => {
const intl = useIntl();
const navigate = useNavigator();
const notify = useNotifier();
const { data, loading } = useOrderSettingsQuery();
const { data, loading } = useOrderSettingsQuery({});
const [
orderSettingsUpdate,
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 ({
automaticallyConfirmAllNewOrders,
automaticallyFulfillNonShippableGiftCard,
fulfillmentAutoApprove,
fulfillmentAllowUnpaid
}: OrderSettingsFormData) => {
const result = await orderSettingsUpdate({
}: OrderSettingsFormData) =>
extractMutationErrors(
orderSettingsUpdate({
variables: {
orderSettingsInput: {
automaticallyFulfillNonShippableGiftCard,
@ -39,25 +58,8 @@ export const OrderSettings: React.FC = () => {
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 (
<OrderSettingsPage

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import {
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(
handleCreate,
updateMetadata,

View file

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

View file

@ -12,6 +12,7 @@ import {
import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { getMutationErrors } from "@saleor/misc";
import usePageSearch from "@saleor/searches/usePageSearch";
import usePageTypeSearch from "@saleor/searches/usePageTypeSearch";
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(
handleCreate,
updateMetadata,

View file

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

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