diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 57e6d926f..daba7e90e 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" }, diff --git a/src/apps/components/AppInstallPage/AppInstallPage.tsx b/src/apps/components/AppInstallPage/AppInstallPage.tsx index ce2f7867a..5ca3ddea6 100644 --- a/src/apps/components/AppInstallPage/AppInstallPage.tsx +++ b/src/apps/components/AppInstallPage/AppInstallPage.tsx @@ -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; } export const AppInstallPage: React.FC = ({ diff --git a/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx b/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx index 2340eb0ac..1ba4e6a14 100644 --- a/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx +++ b/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx @@ -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; } const CustomAppCreatePage: React.FC = props => { @@ -52,7 +55,7 @@ const CustomAppCreatePage: React.FC = props => { const permissionsError = getAppErrorMessage(formErrors.permissions, intl); return ( -
+ {({ data, change, hasChanged, submit }) => ( diff --git a/src/apps/components/CustomAppDetailsPage/CustomAppDetailsPage.tsx b/src/apps/components/CustomAppDetailsPage/CustomAppDetailsPage.tsx index 608879849..6dafd7e43 100644 --- a/src/apps/components/CustomAppDetailsPage/CustomAppDetailsPage.tsx +++ b/src/apps/components/CustomAppDetailsPage/CustomAppDetailsPage.tsx @@ -44,7 +44,9 @@ export interface CustomAppDetailsPageProps { onTokenDelete: (id: string) => void; onTokenClose: () => void; onTokenCreate: () => void; - onSubmit: (data: CustomAppDetailsPageFormData) => SubmitPromise; + onSubmit: ( + data: CustomAppDetailsPageFormData + ) => SubmitPromise; onWebhookCreate: () => void; onWebhookRemove: (id: string) => void; navigateToWebhookDetails: (id: string) => () => void; @@ -94,7 +96,7 @@ const CustomAppDetailsPage: React.FC = props => { }; return ( - + {({ data, change, hasChanged, submit }) => ( diff --git a/src/apps/components/TokenCreateDialog/TokenCreateDialog.tsx b/src/apps/components/TokenCreateDialog/TokenCreateDialog.tsx index 765d3ddd9..3c5c9c86e 100644 --- a/src/apps/components/TokenCreateDialog/TokenCreateDialog.tsx +++ b/src/apps/components/TokenCreateDialog/TokenCreateDialog.tsx @@ -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"; diff --git a/src/apps/views/AppInstall/AppInstall.tsx b/src/apps/views/AppInstall/AppInstall.tsx index 7f2042fb4..bd03413ad 100644 --- a/src/apps/views/AppInstall/AppInstall.tsx +++ b/src/apps/views/AppInstall/AppInstall.tsx @@ -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 = ({ const handleSubmit = () => { const manifest = fetchManifestOpts?.data?.appFetchManifest?.manifest; - installApp({ - variables: { - input: { - appName: manifest?.name, - manifestUrl, - permissions: manifest?.permissions.map(permission => permission.code) + return extractMutationErrors( + installApp({ + variables: { + input: { + appName: manifest?.name, + manifestUrl, + permissions: manifest?.permissions.map( + permission => permission.code + ) + } } - } - }); + }) + ); }; useEffect(() => { diff --git a/src/apps/views/CustomAppCreate/CustomAppCreate.tsx b/src/apps/views/CustomAppCreate/CustomAppCreate.tsx index 844a5c455..508c86e8b 100644 --- a/src/apps/views/CustomAppCreate/CustomAppCreate.tsx +++ b/src/apps/views/CustomAppCreate/CustomAppCreate.tsx @@ -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,17 +42,19 @@ export const CustomAppCreate: React.FC = ({ onCompleted: onSubmit }); - const handleSubmit = (data: CustomAppCreatePageFormData) => - createApp({ - variables: { - input: { - name: data.name, - permissions: data.hasFullAccess - ? shop.permissions.map(permission => permission.code) - : data.permissions + const handleSubmit = async (data: CustomAppCreatePageFormData) => + extractMutationErrors( + createApp({ + variables: { + input: { + name: data.name, + permissions: data.hasFullAccess + ? shop.permissions.map(permission => permission.code) + : data.permissions + } } - } - }); + }) + ); return ( <> diff --git a/src/apps/views/CustomAppDetails/CustomAppDetails.tsx b/src/apps/views/CustomAppDetails/CustomAppDetails.tsx index b7c0c993c..2b0f06704 100644 --- a/src/apps/views/CustomAppDetails/CustomAppDetails.tsx +++ b/src/apps/views/CustomAppDetails/CustomAppDetails.tsx @@ -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,21 +181,20 @@ export const CustomAppDetails: React.FC = ({ onCompleted: onTokenDelete }); - const handleSubmit = async (data: CustomAppDetailsPageFormData) => { - const result = await updateApp({ - variables: { - id, - input: { - name: data.name, - permissions: data.hasFullAccess - ? shop.permissions.map(permission => permission.code) - : data.permissions + const handleSubmit = async (data: CustomAppDetailsPageFormData) => + extractMutationErrors( + updateApp({ + variables: { + id, + input: { + name: data.name, + permissions: data.hasFullAccess + ? shop.permissions.map(permission => permission.code) + : data.permissions + } } - } - }); - - return result.data.appUpdate.errors; - }; + }) + ); const handleTokenCreate = (name: string) => createToken({ diff --git a/src/attributes/components/AttributePage/AttributePage.tsx b/src/attributes/components/AttributePage/AttributePage.tsx index cde878be9..3e1c1109b 100644 --- a/src/attributes/components/AttributePage/AttributePage.tsx +++ b/src/attributes/components/AttributePage/AttributePage.tsx @@ -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 = ({ }; return ( - + {({ change, set, diff --git a/src/attributes/views/AttributeCreate/AttributeCreate.tsx b/src/attributes/views/AttributeCreate/AttributeCreate.tsx index 88321a157..508e4a9c2 100644 --- a/src/attributes/views/AttributeCreate/AttributeCreate.tsx +++ b/src/attributes/views/AttributeCreate/AttributeCreate.tsx @@ -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 = ({ 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, diff --git a/src/attributes/views/AttributeDetails/AttributeDetails.tsx b/src/attributes/views/AttributeDetails/AttributeDetails.tsx index 91a06fb51..2ec2894d3 100644 --- a/src/attributes/views/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/views/AttributeDetails/AttributeDetails.tsx @@ -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 = ({ 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({ - variables: { - id, - input - } - }); - - return result.data?.attributeUpdate.errors; - }; + const handleUpdate = async (data: AttributePageFormData) => + extractMutationErrors( + attributeUpdate({ + variables: { + id, + input: { + ...omit(data, [ + "entityType", + "inputType", + "metadata", + "privateMetadata" + ]), + storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 0) + } + } + }) + ); const handleSubmit = createMetadataUpdateHandler( data?.attribute, diff --git a/src/auth/components/LoginPage/form.tsx b/src/auth/components/LoginPage/form.tsx index b7b32bcf0..9252427d9 100644 --- a/src/auth/components/LoginPage/form.tsx +++ b/src/auth/components/LoginPage/form.tsx @@ -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; + 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 }; } diff --git a/src/auth/components/LoginPage/types.ts b/src/auth/components/LoginPage/types.ts new file mode 100644 index 000000000..0fdd37d7b --- /dev/null +++ b/src/auth/components/LoginPage/types.ts @@ -0,0 +1,4 @@ +export interface LoginFormData { + email: string; + password: string; +} diff --git a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx index 88bb50090..d8d25d67b 100644 --- a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx +++ b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx @@ -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 = { diff --git a/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx index 1c99325b2..89fbba0c9 100644 --- a/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx +++ b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx @@ -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; } const ResetPasswordPage: React.FC = props => { diff --git a/src/auth/views/Login.tsx b/src/auth/views/Login.tsx index ffb93d05f..529d64dcd 100644 --- a/src/auth/views/Login.tsx +++ b/src/auth/views/Login.tsx @@ -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 { diff --git a/src/auth/views/ResetPassword.tsx b/src/auth/views/ResetPassword.tsx index 4d5ec2762..d9606f734 100644 --- a/src/auth/views/ResetPassword.tsx +++ b/src/auth/views/ResetPassword.tsx @@ -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,16 +39,18 @@ const ResetPasswordView: React.FC = () => { {(requestPasswordReset, requestPasswordResetOpts) => { const handleSubmit = (data: ResetPasswordPageFormData) => - requestPasswordReset({ - variables: { - email: data.email, - redirectUrl: urlJoin( - window.location.origin, - APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI, - newPasswordUrl().replace(/\?/, "") - ) - } - }); + extractMutationErrors( + requestPasswordReset({ + variables: { + email: data.email, + redirectUrl: urlJoin( + window.location.origin, + APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI, + newPasswordUrl().replace(/\?/, "") + ) + } + }) + ); return ( { handlers: CategoryCreateHandlers; - hasChanged: boolean; - submit: () => Promise; } export interface CategoryCreateFormProps { @@ -34,20 +35,37 @@ export interface CategoryCreateFormProps { onSubmit: (data: CategoryCreateData) => Promise; } +const initialData: CategoryCreateFormData = { + metadata: [], + name: "", + privateMetadata: [], + seoDescription: "", + seoTitle: "", + slug: "" +}; + function useCategoryCreateForm( onSubmit: (data: CategoryCreateData) => Promise ): UseCategoryCreateFormResult { - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); + const { + handleChange, + data, + hasChanged, + triggerChange, + setChanged, + formId + } = useForm(initialData, undefined, { confirmLeave: true }); - const form = useForm({ - metadata: [], - name: "", - privateMetadata: [], - seoDescription: "", - seoTitle: "", - slug: "" + 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 }; } diff --git a/src/categories/components/CategoryUpdatePage/form.tsx b/src/categories/components/CategoryUpdatePage/form.tsx index 6c9b3695d..a1dd0c6c2 100644 --- a/src/categories/components/CategoryUpdatePage/form.tsx +++ b/src/categories/components/CategoryUpdatePage/form.tsx @@ -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 { handlers: CategoryUpdateHandlers; - hasChanged: boolean; - submit: () => Promise; } export interface CategoryUpdateFormProps { @@ -39,22 +40,39 @@ export interface CategoryUpdateFormProps { onSubmit: (data: CategoryUpdateData) => Promise; } +const getInitialData = (category?: CategoryDetails_category) => ({ + backgroundImageAlt: category?.backgroundImage?.alt || "", + metadata: category?.metadata?.map(mapMetadataItemToInput), + name: category?.name || "", + privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), + seoDescription: category?.seoDescription || "", + seoTitle: category?.seoTitle || "", + slug: category?.slug || "" +}); + function useCategoryUpdateForm( category: CategoryDetails_category, onSubmit: (data: CategoryUpdateData) => Promise ): UseCategoryUpdateFormResult { - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); + const { + handleChange, + data, + triggerChange, + hasChanged, + setChanged, + formId + } = useForm(getInitialData(category), undefined, { confirmLeave: true }); - const form = useForm({ - backgroundImageAlt: category?.backgroundImage?.alt || "", - metadata: category?.metadata?.map(mapMetadataItemToInput), - name: category?.name || "", - privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), - seoDescription: category?.seoDescription || "", - seoTitle: category?.seoTitle || "", - slug: category?.slug || "" + 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 }; } diff --git a/src/categories/views/CategoryCreate.tsx b/src/categories/views/CategoryCreate.tsx index dadb4f964..e4cbdfb52 100644 --- a/src/categories/views/CategoryCreate.tsx +++ b/src/categories/views/CategoryCreate.tsx @@ -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 = ({ } }); - return result.data?.categoryCreate.category?.id || null; + return { + id: result.data?.categoryCreate.category?.id || null, + errors: getMutationErrors(result) + }; }; + const handleSubmit = createMetadataCreateHandler( handleCreate, updateMetadata, diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 8e9d03487..fa6d1bde9 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -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,25 +178,25 @@ export const CategoryDetails: React.FC = ({ paginationState ); - const handleUpdate = async (formData: CategoryUpdateData) => { - const result = await updateCategory({ - variables: { - id, - input: { - backgroundImageAlt: formData.backgroundImageAlt, - description: getParsedDataForJsonStringField(formData.description), - name: formData.name, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - }, - slug: formData.slug + const handleUpdate = async (formData: CategoryUpdateData) => + extractMutationErrors( + updateCategory({ + variables: { + id, + input: { + backgroundImageAlt: formData.backgroundImageAlt, + description: getParsedDataForJsonStringField(formData.description), + name: formData.name, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + }, + slug: formData.slug + } } - } - }); + }) + ); - return result.data.categoryUpdate.errors; - }; const handleSubmit = createMetadataUpdateHandler( data?.category, handleUpdate, diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx index 8a227c045..d401781b1 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx @@ -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 = { currencyCodes: [ { label: "USD", value: "USD" }, { label: "PLN", value: "PLN" } diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx index 1ac6af77a..8221a39a8 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx @@ -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 { 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; updateChannelStatus?: () => void; searchShippingZones: (query: string) => void; } -export const ChannelDetailsPage: React.FC = ({ +const ChannelDetailsPage = function({ channel, currencyCodes, disabled, @@ -61,7 +62,7 @@ export const ChannelDetailsPage: React.FC = ({ fetchMoreShippingZones, countries, channelShippingZones = [] -}) => { +}: ChannelDetailsPageProps) { const [selectedCurrencyCode, setSelectedCurrencyCode] = useState(""); const [ selectedCountryDisplayName, @@ -92,7 +93,7 @@ export const ChannelDetailsPage: React.FC = ({ ); return ( - + {({ change, data, hasChanged, submit, set }) => { const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler( change, diff --git a/src/channels/views/ChannelCreate/ChannelCreate.tsx b/src/channels/views/ChannelCreate/ChannelCreate.tsx index 8c7094a7d..633cf96c4 100644 --- a/src/channels/views/ChannelCreate/ChannelCreate.tsx +++ b/src/channels/views/ChannelCreate/ChannelCreate.tsx @@ -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,15 +45,17 @@ export const ChannelCreateView = ({}) => { currencyCode, ...rest }: FormData) => - createChannel({ - variables: { - input: { - ...rest, - currencyCode: currencyCode.toUpperCase(), - addShippingZones: shippingZonesIdsToAdd + extractMutationErrors( + createChannel({ + variables: { + input: { + ...rest, + currencyCode: currencyCode.toUpperCase(), + addShippingZones: shippingZonesIdsToAdd + } } - } - }); + }) + ); const { loadMore: fetchMoreShippingZones, diff --git a/src/channels/views/ChannelDetails/ChannelDetails.tsx b/src/channels/views/ChannelDetails/ChannelDetails.tsx index 63a723b2c..1c4b2edbd 100644 --- a/src/channels/views/ChannelDetails/ChannelDetails.tsx +++ b/src/channels/views/ChannelDetails/ChannelDetails.tsx @@ -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,18 +106,20 @@ export const ChannelDetails: React.FC = ({ shippingZonesIdsToAdd, defaultCountry }: FormData) => - updateChannel({ - variables: { - id: data?.channel.id, - input: { - name, - slug, - defaultCountry, - addShippingZones: shippingZonesIdsToAdd, - removeShippingZones: shippingZonesIdsToRemove + extractMutationErrors( + updateChannel({ + variables: { + id: data?.channel.id, + input: { + name, + slug, + defaultCountry, + addShippingZones: shippingZonesIdsToAdd, + removeShippingZones: shippingZonesIdsToRemove + } } - } - }); + }) + ); const onDeleteCompleted = (data: ChannelDelete) => { const errors = data.channelDelete.errors; diff --git a/src/collections/components/CollectionCreatePage/form.tsx b/src/collections/components/CollectionCreatePage/form.tsx index f76021470..fedcdf545 100644 --- a/src/collections/components/CollectionCreatePage/form.tsx +++ b/src/collections/components/CollectionCreatePage/form.tsx @@ -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 ) => void; } -export interface UseCollectionCreateFormResult { - change: FormChange; - data: CollectionCreateData; - handlers: CollectionCreateHandlers; - hasChanged: boolean; - submit: () => Promise; -} +export type UseCollectionCreateFormResult = CommonUseFormResultWithHandlers< + CollectionCreateData, + CollectionCreateHandlers +>; export interface CollectionCreateFormProps { currentChannels: ChannelCollectionData[]; @@ -48,28 +51,50 @@ export interface CollectionCreateFormProps { onSubmit: (data: CollectionCreateData) => SubmitPromise; } +const getInitialData = ( + currentChannels: ChannelCollectionData[] +): CollectionCreateFormData => ({ + backgroundImage: { + url: null, + value: null + }, + backgroundImageAlt: "", + channelListings: currentChannels, + metadata: [], + name: "", + privateMetadata: [], + seoDescription: "", + seoTitle: "", + slug: "" +}); + 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({ - backgroundImage: { - url: null, - value: null - }, - backgroundImageAlt: "", - channelListings: currentChannels, - metadata: [], - name: "", - privateMetadata: [], - seoDescription: "", - seoTitle: "", - slug: "" + 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 }; } diff --git a/src/collections/components/CollectionDetailsPage/form.tsx b/src/collections/components/CollectionDetailsPage/form.tsx index d65589a04..6d909062b 100644 --- a/src/collections/components/CollectionDetailsPage/form.tsx +++ b/src/collections/components/CollectionDetailsPage/form.tsx @@ -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 ) => void; } -export interface UseCollectionUpdateFormResult { - change: FormChange; - data: CollectionUpdateData; - handlers: CollectionUpdateHandlers; - hasChanged: boolean; - submit: () => Promise; -} +export type UseCollectionUpdateFormResult = CommonUseFormResultWithHandlers< + CollectionUpdateData, + CollectionUpdateHandlers +>; export interface CollectionUpdateFormProps { children: (props: UseCollectionUpdateFormResult) => React.ReactNode; @@ -48,25 +50,48 @@ export interface CollectionUpdateFormProps { onSubmit: (data: CollectionUpdateData) => Promise; } +const getInitialData = ( + collection: CollectionDetails_collection, + currentChannels: ChannelCollectionData[] +): CollectionUpdateFormData => ({ + backgroundImageAlt: collection?.backgroundImage?.alt || "", + channelListings: currentChannels, + metadata: collection?.metadata?.map(mapMetadataItemToInput), + name: collection?.name || "", + privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput), + seoDescription: collection?.seoDescription || "", + seoTitle: collection?.seoTitle || "", + slug: collection?.slug || "" +}); + function useCollectionUpdateForm( collection: CollectionDetails_collection, currentChannels: ChannelCollectionData[], setChannels: (data: ChannelCollectionData[]) => void, onSubmit: (data: CollectionUpdateData) => Promise ): UseCollectionUpdateFormResult { - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); - - const form = useForm({ - backgroundImageAlt: collection?.backgroundImage?.alt || "", - channelListings: currentChannels, - metadata: collection?.metadata?.map(mapMetadataItemToInput), - name: collection?.name || "", - privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput), - seoDescription: collection?.seoDescription || "", - seoTitle: collection?.seoTitle || "", - slug: collection?.slug || "" + 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 }; } diff --git a/src/collections/views/CollectionCreate.tsx b/src/collections/views/CollectionCreate.tsx index 26adfb16e..3356bb583 100644 --- a/src/collections/views/CollectionCreate.tsx +++ b/src/collections/views/CollectionCreate.tsx @@ -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 = ({ 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 = ({ }); } - return id; + return { id, errors: getMutationErrors(result) }; }; const handleSubmit = createMetadataCreateHandler( diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index db41f31d1..027c1cfa1 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -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 = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(collectionChannelsChoices, params?.action, { - closeModal, - openModal - }); + } = 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 = ({ } }); - return result.data.collectionUpdate.errors; + return getMutationErrors(result); }; + const handleSubmit = createMetadataUpdateHandler( data?.collection, handleUpdate, diff --git a/src/collections/views/consts.ts b/src/collections/views/consts.ts new file mode 100644 index 000000000..cbc23fbe5 --- /dev/null +++ b/src/collections/views/consts.ts @@ -0,0 +1,2 @@ +export const COLLECTION_DETAILS_FORM_ID = Symbol(); +export const COLLECTION_CREATE_FORM_ID = Symbol(); diff --git a/src/components/CardTitle/CardTitle.tsx b/src/components/CardTitle/CardTitle.tsx index 2fc69d778..788dd6f6f 100644 --- a/src/components/CardTitle/CardTitle.tsx +++ b/src/components/CardTitle/CardTitle.tsx @@ -9,6 +9,7 @@ interface CardTitleProps { subtitle?: string | React.ReactNode; toolbar?: React.ReactNode; onClick?: (event: React.MouseEvent) => void; + onClose?: () => void; } const CardTitle: React.FC = ({ diff --git a/src/components/EditableTableCell/EditableTableCell.tsx b/src/components/EditableTableCell/EditableTableCell.tsx index 164eec078..b17a14f74 100644 --- a/src/components/EditableTableCell/EditableTableCell.tsx +++ b/src/components/EditableTableCell/EditableTableCell.tsx @@ -59,18 +59,20 @@ export const EditableTableCell: React.FC = 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); diff --git a/src/components/Form/ExitFormDialog.tsx b/src/components/Form/ExitFormDialog.tsx new file mode 100644 index 000000000..37aa57672 --- /dev/null +++ b/src/components/Form/ExitFormDialog.tsx @@ -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 = ({ + onSubmit, + onLeave, + onClose, + isOpen +}) => { + const classes = useStyles(); + const intl = useIntl(); + + return ( + + + + + + +
+ + + +
+
+
+ ); +}; + +export default ExitFormDialog; diff --git a/src/components/Form/ExitFormDialogProvider.tsx b/src/components/Form/ExitFormDialogProvider.tsx new file mode 100644 index 000000000..6e653000e --- /dev/null +++ b/src/components/Form/ExitFormDialogProvider.tsx @@ -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; + +export type FormId = symbol; + +type FormsData = Record; + +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({ + 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({}); + 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) => { + 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 = SubmitPromise>( + 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 ( + + + {children} + + ); +}; + +export default ExitFormDialogProvider; diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 28280163d..5874ff431 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -1,18 +1,28 @@ import useForm, { SubmitPromise, UseFormResult } from "@saleor/hooks/useForm"; import React from "react"; -export interface FormProps +import { FormId } from "./ExitFormDialogProvider"; + +export interface FormProps extends Omit, "onSubmit"> { - children: (props: UseFormResult) => React.ReactNode; + children: (props: UseFormResult) => React.ReactNode; confirmLeave?: boolean; - initial?: T; + initial?: TData; resetOnSubmit?: boolean; - onSubmit?: (data: T) => SubmitPromise | void; + onSubmit?: (data: TData) => SubmitPromise | void; + formId?: FormId; } -function Form(props: FormProps) { - const { children, initial, resetOnSubmit, onSubmit, ...rest } = props; - const renderProps = useForm(initial, onSubmit); +function Form({ + children, + initial, + resetOnSubmit, + onSubmit, + confirmLeave = false, + formId, + ...rest +}: FormProps) { + const renderProps = useForm(initial, onSubmit, { confirmLeave, formId }); function handleSubmit(event?: React.FormEvent, cb?: () => void) { const { reset, submit } = renderProps; diff --git a/src/components/Form/messages.ts b/src/components/Form/messages.ts new file mode 100644 index 000000000..73ca90845 --- /dev/null +++ b/src/components/Form/messages.ts @@ -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" + } +}); diff --git a/src/components/Form/useExitFormDialog.ts b/src/components/Form/useExitFormDialog.ts new file mode 100644 index 000000000..b249cf70f --- /dev/null +++ b/src/components/Form/useExitFormDialog.ts @@ -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) + }; +}; diff --git a/src/components/SaveButtonBar/ButtonTooltipDecorator.tsx b/src/components/SaveButtonBar/ButtonTooltipDecorator.tsx deleted file mode 100644 index 648213c3c..000000000 --- a/src/components/SaveButtonBar/ButtonTooltipDecorator.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Tooltip } from "@material-ui/core"; -import React from "react"; - -interface ButtonTooltipDecoratorProps { - tooltip?: string; -} - -export const ButtonTooltipDecorator: React.FC = ({ - tooltip, - children -}) => { - if (tooltip) { - return ( - - {children} - - ); - } - - return <>{children}; -}; -ButtonTooltipDecorator.displayName = "ButtonTooltipDecorator"; -export default ButtonTooltipDecorator; diff --git a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx index 7effffb41..93eb8c4e1 100644 --- a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx +++ b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx @@ -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 = ({ @@ -117,20 +119,22 @@ const CustomerCreatePage: React.FC = ({ .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 ( - + {({ change, data, hasChanged, submit }) => { const handleCountrySelect = createSingleAutocompleteSelectHandler( change, diff --git a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx index b29004c5f..4a28de28a 100644 --- a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx +++ b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx @@ -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; onViewAllOrdersClick: () => void; onRowClick: (id: string) => void; onAddressManageClick: () => void; @@ -75,7 +78,7 @@ const CustomerDetailsPage: React.FC = ({ } = useMetadataChangeTrigger(); return ( - + {({ change, data, hasChanged, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); diff --git a/src/customers/views/CustomerCreate.tsx b/src/customers/views/CustomerCreate.tsx index 0331f9416..75d8d386c 100644 --- a/src/customers/views/CustomerCreate.tsx +++ b/src/customers/views/CustomerCreate.tsx @@ -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,41 +27,47 @@ export const CustomerCreate: React.FC<{}> = () => { navigate(customerUrl(data.customerCreate.user.id)); } }; + return ( {({ data, loading }) => ( - {(createCustomer, createCustomerOpts) => ( - <> - - data.shop.countries, [])} - disabled={loading || createCustomerOpts.loading} - 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 - } + {(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 ( + <> + + data.shop.countries, [])} + disabled={loading || createCustomerOpts.loading} + errors={createCustomerOpts.data?.customerCreate.errors || []} + saveButtonBar={createCustomerOpts.status} + onBack={() => navigate(customerListUrl())} + onSubmit={handleSubmit} + /> + + ); + }} )} diff --git a/src/customers/views/CustomerDetailsContent.tsx b/src/customers/views/CustomerDetailsContent.tsx index e0046af3f..c82afcbaf 100644 --- a/src/customers/views/CustomerDetailsContent.tsx +++ b/src/customers/views/CustomerDetailsContent.tsx @@ -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,22 +69,21 @@ export const CustomerDetailsContent: React.FC = ({ const [updateMetadata] = useMetadataUpdate({}); const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); - const updateData = async (data: CustomerDetailsPageFormData) => { - const result = await updateCustomer({ - variables: { - id, - input: { - email: data.email, - firstName: data.firstName, - isActive: data.isActive, - lastName: data.lastName, - note: data.note + const updateData = async (data: CustomerDetailsPageFormData) => + extractMutationErrors( + updateCustomer({ + variables: { + id, + input: { + email: data.email, + firstName: data.firstName, + isActive: data.isActive, + lastName: data.lastName, + note: data.note + } } - } - }); - - return result.data.customerUpdate.errors; - }; + }) + ); const handleSubmit = createMetadataUpdateHandler( user, diff --git a/src/discounts/components/DiscountCountrySelectDialog/DiscountCountrySelectDialog.tsx b/src/discounts/components/DiscountCountrySelectDialog/DiscountCountrySelectDialog.tsx index aabffac84..d14aae884 100644 --- a/src/discounts/components/DiscountCountrySelectDialog/DiscountCountrySelectDialog.tsx +++ b/src/discounts/components/DiscountCountrySelectDialog/DiscountCountrySelectDialog.tsx @@ -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 = props => { @@ -90,7 +91,9 @@ const DiscountCountrySelectDialog: React.FC = 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" diff --git a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx index cc492c049..ffb24281a 100644 --- a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx +++ b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx @@ -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; } const SaleCreatePage: React.FC = ({ @@ -79,8 +81,14 @@ const SaleCreatePage: React.FC = ({ metadata: [], privateMetadata: [] }; + return ( - + {({ change, data, hasChanged, submit, triggerChange }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, diff --git a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx index 61abad3c8..366ab4f99 100644 --- a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx +++ b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx @@ -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; onTabClick: (index: SaleDetailsPageTab) => void; onChannelsChange: (data: ChannelSaleFormData[]) => void; openChannelsModal: () => void; @@ -157,7 +159,12 @@ const SaleDetailsPage: React.FC = ({ privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput) }; return ( - + {({ change, data, hasChanged, submit, triggerChange }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index cae55c4fb..a4fbfd3cc 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -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 = ({ @@ -104,7 +90,12 @@ const VoucherCreatePage: React.FC = ({ }; return ( - + {({ change, data, hasChanged, submit, triggerChange, set }) => { const handleDiscountTypeChange = createDiscountTypeChangeHandler( change diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 038503377..e945bd2e2 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -192,7 +192,7 @@ const VoucherDetailsPage: React.FC = ({ }; return ( - + {({ change, data, hasChanged, submit, triggerChange, set }) => { const handleDiscountTypeChange = createDiscountTypeChangeHandler( change diff --git a/src/discounts/views/SaleCreate/SaleCreate.tsx b/src/discounts/views/SaleCreate/SaleCreate.tsx index 716e0657f..84a20a49c 100644 --- a/src/discounts/views/SaleCreate/SaleCreate.tsx +++ b/src/discounts/views/SaleCreate/SaleCreate.tsx @@ -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 = ({ 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({}); diff --git a/src/discounts/views/SaleCreate/consts.ts b/src/discounts/views/SaleCreate/consts.ts new file mode 100644 index 000000000..18656e449 --- /dev/null +++ b/src/discounts/views/SaleCreate/consts.ts @@ -0,0 +1 @@ +export const SALE_CREATE_FORM_ID = Symbol(); diff --git a/src/discounts/views/SaleCreate/handlers.ts b/src/discounts/views/SaleCreate/handlers.ts index 938f40122..1a1b421b3 100644 --- a/src/discounts/views/SaleCreate/handlers.ts +++ b/src/discounts/views/SaleCreate/handlers.ts @@ -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 }; }; } diff --git a/src/discounts/views/SaleDetails/SaleDetails.tsx b/src/discounts/views/SaleDetails/SaleDetails.tsx index c77f07613..a075b36fa 100644 --- a/src/discounts/views/SaleDetails/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails/SaleDetails.tsx @@ -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 = ({ id, params }) => { isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(saleChannelsChoices, params?.action, { - closeModal, - openModal - }); + } = useChannels( + saleChannelsChoices, + params?.action, + { + closeModal, + openModal + }, + { formId: SALE_UPDATE_FORM_ID } + ); const [selectedChannel] = useLocalStorage("salesListChannel", ""); diff --git a/src/discounts/views/SaleDetails/types.ts b/src/discounts/views/SaleDetails/types.ts new file mode 100644 index 000000000..a830aa712 --- /dev/null +++ b/src/discounts/views/SaleDetails/types.ts @@ -0,0 +1 @@ +export const SALE_UPDATE_FORM_ID = Symbol(); diff --git a/src/discounts/views/VoucherCreate/VoucherCreate.tsx b/src/discounts/views/VoucherCreate/VoucherCreate.tsx index 90a58301e..0f2ca9d87 100644 --- a/src/discounts/views/VoucherCreate/VoucherCreate.tsx +++ b/src/discounts/views/VoucherCreate/VoucherCreate.tsx @@ -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 = ({ 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( {} diff --git a/src/discounts/views/VoucherCreate/handlers.ts b/src/discounts/views/VoucherCreate/handlers.ts index cf4487d11..b396b9a81 100644 --- a/src/discounts/views/VoucherCreate/handlers.ts +++ b/src/discounts/views/VoucherCreate/handlers.ts @@ -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 }; }; } diff --git a/src/discounts/views/VoucherCreate/types.ts b/src/discounts/views/VoucherCreate/types.ts new file mode 100644 index 000000000..42f9f2274 --- /dev/null +++ b/src/discounts/views/VoucherCreate/types.ts @@ -0,0 +1 @@ +export const VOUCHER_CREATE_FORM_ID = Symbol(); diff --git a/src/discounts/views/VoucherDetails/VoucherDetails.tsx b/src/discounts/views/VoucherDetails/VoucherDetails.tsx index 8f8522c1e..ca8cfc280 100644 --- a/src/discounts/views/VoucherDetails/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails/VoucherDetails.tsx @@ -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 = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(voucherChannelsChoices, params?.action, { - closeModal, - openModal - }); + } = useChannels( + voucherChannelsChoices, + params?.action, + { + closeModal, + openModal + }, + { formId: VOUCHER_UPDATE_FORM_ID } + ); const [updateChannels, updateChannelsOpts] = useVoucherChannelListingUpdate( {} diff --git a/src/discounts/views/VoucherDetails/types.ts b/src/discounts/views/VoucherDetails/types.ts new file mode 100644 index 000000000..4f5f0f7ef --- /dev/null +++ b/src/discounts/views/VoucherDetails/types.ts @@ -0,0 +1 @@ +export const VOUCHER_UPDATE_FORM_ID = Symbol(); diff --git a/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx b/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx index 842e0a9c0..9103ad7ed 100644 --- a/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx +++ b/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx @@ -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 = ({ return result?.data?.giftCardUpdate?.errors; }; - const formProps = useForm(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 = ({ ...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"], diff --git a/src/hooks/useAddressValidation.ts b/src/hooks/useAddressValidation.ts index 5385dfce0..e9a66802e 100644 --- a/src/hooks/useAddressValidation.ts +++ b/src/hooks/useAddressValidation.ts @@ -11,7 +11,9 @@ import { useState } from "react"; interface UseAddressValidation { errors: AccountErrorFragment[]; - submit: (data: TInput & AddressTypeInput) => TOutput; + submit: ( + data: TInput & AddressTypeInput + ) => TOutput | Promise; } function useAddressValidation( @@ -42,7 +44,10 @@ function useAddressValidation( ); return onSubmit(transformFormToAddressInput(data)); } catch { - setValidationErrors(add(countryRequiredError, validationErrors)); + const errors = add(countryRequiredError, validationErrors); + setValidationErrors(errors); + // since every onSubmit must return Promise + return Promise.resolve(errors); } } }; diff --git a/src/hooks/useChannels.test.ts b/src/hooks/useChannels.test.ts index 84446bde4..d57e42a62 100644 --- a/src/hooks/useChannels.test.ts +++ b/src/hooks/useChannels.test.ts @@ -20,10 +20,15 @@ describe("useChannels", () => { it("properly toggles channels", () => { // Given const { result } = renderHook(() => - useChannels(channels, "", { - closeModal: jest.fn, - openModal: jest.fn - }) + 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, "", { - closeModal: jest.fn, - openModal: jest.fn - }) + 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, "", { - closeModal: jest.fn, - openModal: jest.fn - }) + useChannels( + channels, + "", + { + closeModal: jest.fn, + openModal: jest.fn + }, + { formId: Symbol() } + ) ); // When diff --git a/src/hooks/useChannels.ts b/src/hooks/useChannels.ts index 90b539d21..2730a867a 100644 --- a/src/hooks/useChannels.ts +++ b/src/hooks/useChannels.ts @@ -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( 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( (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(); }; diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index 85d1a5a69..423a99a3a 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -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 { value: TData; }; } -export type SubmitPromise = Promise; +export type SubmitPromise = Promise; export type FormChange = (event: ChangeEvent, cb?: () => void) => void; @@ -20,19 +26,35 @@ export type FormErrors = { [field in keyof T]?: string | React.ReactNode; }; -export interface UseFormResult { - change: FormChange; - data: T; - hasChanged: boolean; +export interface UseFormOpts { + confirmLeave: boolean; + formId?: FormId; +} + +export interface UseFormResult + extends CommonUseFormResult, + Pick { reset: () => void; - set: (data: Partial) => void; - submit: () => void; + set: (data: Partial) => void; triggerChange: () => void; - toggleValue: FormChange; - errors: FormErrors; setChanged: (value: boolean) => void; - setError: (name: keyof T, error: string | React.ReactNode) => void; - clearErrors: (name?: keyof T | Array) => void; + handleChange: FormChange; + toggleValue: FormChange; + errors: FormErrors; + setError: (name: keyof TData, error: string | React.ReactNode) => void; + clearErrors: (name?: keyof TData | Array) => void; +} + +export interface CommonUseFormResult { + data: TData; + change: FormChange; + hasChanged: boolean; + submit: (dataOrEvent?: any) => SubmitPromise; +} + +export interface CommonUseFormResultWithHandlers + extends CommonUseFormResult { + handlers: THandlers; } type FormData = Record; @@ -60,25 +82,61 @@ function handleRefresh( } } -function useForm( - initial: T, - onSubmit?: (data: T) => SubmitPromise | void +function useForm( + initialData: T, + onSubmit?: (data: T) => SubmitPromise | void, + opts: UseFormOpts = { confirmLeave: false, formId: undefined } ): UseFormResult { + const { confirmLeave, formId: propsFormId } = opts; const [hasChanged, setChanged] = useState(false); const [errors, setErrors] = useState>({}); - 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( } } + const handleChange: FormChange = event => { + change(event); + handleSetChanged(true); + }; + function change(event: ChangeEvent) { const { name, value } = event.target; @@ -98,7 +161,7 @@ function useForm( return; } else { if (data[name] !== value) { - setChanged(true); + handleSetChanged(true); } setData(data => ({ ...data, @@ -108,7 +171,7 @@ function useForm( } function reset() { - setData(initial); + setData(initialData); } function set(newData: Partial, setHasChanged = true) { @@ -116,23 +179,15 @@ function useForm( ...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( }; return { + formId, setError, errors, change, @@ -156,11 +212,12 @@ function useForm( data, hasChanged, reset, - setChanged, set, submit, toggleValue, - triggerChange + handleChange, + triggerChange: handleSetChanged, + setChanged: handleSetChanged }; } diff --git a/src/hooks/useHandleFormSubmit.ts b/src/hooks/useHandleFormSubmit.ts new file mode 100644 index 000000000..ac1a51e4f --- /dev/null +++ b/src/hooks/useHandleFormSubmit.ts @@ -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 { + formId?: FormId; + onSubmit: (data: TData) => SubmitPromise | void; + setChanged: (changed: boolean) => void; +} + +function useHandleFormSubmit({ + formId, + onSubmit, + setChanged +}: UseHandleFormSubmitProps) { + const { setIsSubmitting } = useExitFormDialog({ + formId + }); + + async function handleFormSubmit(data: TData): Promise { + 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; diff --git a/src/hooks/useNavigator.ts b/src/hooks/useNavigator.ts index 4410f7178..53dc3ae59 100644 --- a/src/hooks/useNavigator.ts +++ b/src/hooks/useNavigator.ts @@ -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 { diff --git a/src/index.tsx b/src/index.tsx index c0e43bd9f..f4afc6354 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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 = () => ( - + + + diff --git a/src/misc.ts b/src/misc.ts index 64b0343f3..80d719240 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -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 function getMutationErrors< - TData extends Record ->(data: TData): UserError[] { - return Object.values(data).reduce( - (acc: UserError[], mut) => [...acc, ...maybe(() => mut.errors, [])], - [] - ); +export interface SaleorMutationResult { + errors?: any[]; } + +type InferPromiseResult = T extends Promise ? V : never; + +export const extractMutationErrors = async < + TData extends InferPromiseResult, + TPromise extends Promise>, + TErrors extends ReturnType +>( + submitPromise: TPromise +): Promise => { + const result = await submitPromise; + + const e = getMutationErrors(result); + + return e as TErrors; +}; + +export const getMutationErrors = < + T extends MutationFetchResult, + 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 >(opts: MutationResult): ConfirmButtonTransitionState { - const errors = opts.data ? getMutationErrors(opts.data) : []; + const errors = getMutationErrors(opts); return getMutationState(opts.called, opts.loading, errors); } diff --git a/src/navigation/components/MenuDetailsPage/MenuDetailsPage.tsx b/src/navigation/components/MenuDetailsPage/MenuDetailsPage.tsx index 69492490e..6a15846d7 100644 --- a/src/navigation/components/MenuDetailsPage/MenuDetailsPage.tsx +++ b/src/navigation/components/MenuDetailsPage/MenuDetailsPage.tsx @@ -82,7 +82,7 @@ const MenuDetailsPage: React.FC = ({ }; return ( - + {({ change, data, hasChanged, submit }) => ( diff --git a/src/navigation/views/MenuDetails/index.tsx b/src/navigation/views/MenuDetails/index.tsx index fedf84942..432e6f109 100644 --- a/src/navigation/views/MenuDetails/index.tsx +++ b/src/navigation/views/MenuDetails/index.tsx @@ -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 = ({ 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 = ({ id, params }) => { input: getMenuItemCreateInputData(id, data) }; - menuItemCreate({ variables }); + extractMutationErrors( + menuItemCreate({ variables }) + ); }; return ( @@ -268,7 +274,9 @@ const MenuDetails: React.FC = ({ id, params }) => { input: getMenuItemInputData(data) }; - menuItemUpdate({ variables }); + extractMutationErrors( + menuItemUpdate({ variables }) + ); }; const menuItem = maybe(() => diff --git a/src/orders/components/OrderCustomer/OrderCustomer.tsx b/src/orders/components/OrderCustomer/OrderCustomer.tsx index 895dc1762..8d50eb455 100644 --- a/src/orders/components/OrderCustomer/OrderCustomer.tsx +++ b/src/orders/components/OrderCustomer/OrderCustomer.tsx @@ -150,7 +150,7 @@ const OrderCustomer: React.FC = props => { {user === undefined ? ( ) : isInEditMode && canEditCustomer ? ( - + {({ change, data }) => { const handleChange = (event: React.ChangeEvent) => { change(event); diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesEditDialog.tsx b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesEditDialog.tsx index 28495fb29..88536d2f4 100644 --- a/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesEditDialog.tsx +++ b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesEditDialog.tsx @@ -58,7 +58,7 @@ export interface OrderCustomerAddressesEditDialogProps { defaultShippingAddress?: CustomerAddresses_user_defaultShippingAddress; defaultBillingAddress?: CustomerAddresses_user_defaultBillingAddress; onClose(); - onConfirm(data: OrderCustomerAddressesEditDialogOutput): SubmitPromise; + onConfirm(data: OrderCustomerAddressesEditDialogOutput): SubmitPromise; } const defaultSearchState: OrderCustomerSearchAddressState = { @@ -133,8 +133,13 @@ const OrderCustomerAddressesEditDialog: React.FC) => void; - change: FormChange; - hasChanged: boolean; - data: OrderCustomerAddressesEditData; - handlers: OrderCustomerAddressesEditHandlers; +interface UseOrderCustomerAddressesEditFormResult + extends CommonUseFormResultWithHandlers< + OrderCustomerAddressesEditData, + OrderCustomerAddressesEditHandlers + > { + submit: (event: React.FormEvent) => SubmitPromise; } interface UseOrderCustomerAddressesEditFormOpts { @@ -61,38 +67,46 @@ export interface OrderCustomerAddressesEditFormProps extends UseOrderCustomerAddressesEditFormOpts { children: (props: UseOrderCustomerAddressesEditFormResult) => React.ReactNode; initial?: Partial; - onSubmit: (data: OrderCustomerAddressesEditData) => void; + onSubmit: (data: OrderCustomerAddressesEditData) => SubmitPromise; } +const initialAddress: AddressTypeInput = { + city: "", + country: "", + phone: "", + postalCode: "", + streetAddress1: "" +}; + +const getDefaultInitialFormData = ( + opts: UseOrderCustomerAddressesEditFormOpts +): OrderCustomerAddressesEditFormData => ({ + billingSameAsShipping: true, + shippingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, + billingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, + customerShippingAddress: opts.defaultShippingAddress, + customerBillingAddress: opts.defaultBillingAddress, + shippingAddress: initialAddress, + billingAddress: initialAddress +}); + function useOrderCustomerAddressesEditForm( initial: Partial, - onSubmit: (data: OrderCustomerAddressesEditData) => void, + onSubmit: (data: OrderCustomerAddressesEditData) => SubmitPromise, opts: UseOrderCustomerAddressesEditFormOpts ): UseOrderCustomerAddressesEditFormResult { - const initialAddress: AddressTypeInput = { - city: "", - country: "", - phone: "", - postalCode: "", - streetAddress1: "" - }; - const defaultInitialFormData: OrderCustomerAddressesEditFormData = { - billingSameAsShipping: true, - shippingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, - billingAddressInputOption: AddressInputOptionEnum.CUSTOMER_ADDRESS, - customerShippingAddress: opts.defaultShippingAddress, - customerBillingAddress: opts.defaultBillingAddress, - shippingAddress: initialAddress, - billingAddress: initialAddress - }; - - const form = useForm({ + const { + handleChange, + hasChanged, + change, + data: formData, + setChanged + } = useForm({ ...initial, - ...defaultInitialFormData + ...getDefaultInitialFormData(opts) }); - const [changed, setChanged] = useState(false); - const triggerChange = () => setChanged(true); + 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, 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) => { event.stopPropagation(); event.preventDefault(); - return onSubmit(data); + return handleSubmit(); }; + useEffect(() => setExitDialogSubmitRef(submit), [handleSubmit]); + return { change: handleChange, submit, - hasChanged: changed, + hasChanged, data, handlers: { changeCustomerAddress: handleCustomerAddressChange, diff --git a/src/orders/components/OrderCustomerChangeDialog/OrderCustomerChangeDialog.tsx b/src/orders/components/OrderCustomerChangeDialog/OrderCustomerChangeDialog.tsx index e20eb6e7a..495923aa1 100644 --- a/src/orders/components/OrderCustomerChangeDialog/OrderCustomerChangeDialog.tsx +++ b/src/orders/components/OrderCustomerChangeDialog/OrderCustomerChangeDialog.tsx @@ -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 = props => { diff --git a/src/orders/components/OrderCustomerChangeDialog/form.tsx b/src/orders/components/OrderCustomerChangeDialog/form.tsx index dc5168dc8..d5683ae9b 100644 --- a/src/orders/components/OrderCustomerChangeDialog/form.tsx +++ b/src/orders/components/OrderCustomerChangeDialog/form.tsx @@ -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) => 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; } +const defaultInitialFormData: OrderCustomerChangeData = { + changeActionOption: CustomerChangeActionEnum.KEEP_ADDRESS +}; + function useOrderCustomerChangeForm( - initial: Partial, + initial: Partial = {}, onSubmit: (data: OrderCustomerChangeData) => void ): UseOrderCustomerChangeFormResult { - const defaultInitialFormData: OrderCustomerChangeData = { - changeActionOption: CustomerChangeActionEnum.KEEP_ADDRESS - }; - - const form = useForm({ + 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) => { 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 = ({ initial, onSubmit }) => { - const props = useOrderCustomerChangeForm(initial || {}, onSubmit); + const props = useOrderCustomerChangeForm(initial, onSubmit); return {children(props)}; }; diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 45c274b09..2372d4db1 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -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 = props => { privateMetadata }); resetMetadataChanged(); - return result; + return getMutationErrors(result); }; const initial: MetadataFormData = { @@ -212,7 +212,7 @@ const OrderDetailsPage: React.FC = props => { ]); return ( -
+ {({ change, data, hasChanged, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index 19b67a993..eb7cd4848 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -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; onOrderLineAdd: () => void; onOrderLineChange: ( id: string, diff --git a/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx b/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx index 8ade9e719..ffc0fd53c 100644 --- a/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx +++ b/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx @@ -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; } 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 = props => { orderNumber: order?.number })} /> - + {({ change, data, submit }) => ( <> diff --git a/src/orders/components/OrderFulfillmentCancelDialog/OrderFulfillmentCancelDialog.tsx b/src/orders/components/OrderFulfillmentCancelDialog/OrderFulfillmentCancelDialog.tsx index 4adf543c2..3a3240a68 100644 --- a/src/orders/components/OrderFulfillmentCancelDialog/OrderFulfillmentCancelDialog.tsx +++ b/src/orders/components/OrderFulfillmentCancelDialog/OrderFulfillmentCancelDialog.tsx @@ -76,7 +76,7 @@ const OrderFulfillmentCancelDialog: React.FC fullWidth maxWidth="sm" > - + {({ change, data: formData, submit }) => { const handleChange = createSingleAutocompleteSelectHandler( change, diff --git a/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx b/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx index df7b2776e..8602e59e6 100644 --- a/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx +++ b/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx @@ -48,7 +48,7 @@ const OrderFulfillmentTrackingDialog: React.FC - + {({ change, data, submit }) => ( <> diff --git a/src/orders/components/OrderHistory/OrderHistory.tsx b/src/orders/components/OrderHistory/OrderHistory.tsx index 296e245ea..5a21e4cc2 100644 --- a/src/orders/components/OrderHistory/OrderHistory.tsx +++ b/src/orders/components/OrderHistory/OrderHistory.tsx @@ -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 = props => { @@ -331,7 +332,12 @@ const OrderHistory: React.FC = props => {
{history ? ( - + {({ change, data, reset, submit }) => ( { disabled: boolean; - handlers: OrderRefundHandlers; - hasChanged: boolean; - submit: () => Promise; } 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( 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 = ( 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 }; } diff --git a/src/orders/components/OrderReturnPage/form.tsx b/src/orders/components/OrderReturnPage/form.tsx index 5c5655915..04c8f2a4f 100644 --- a/src/orders/components/OrderReturnPage/form.tsx +++ b/src/orders/components/OrderReturnPage/form.tsx @@ -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; -} +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( 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(callback: (id: string, value: T) => void) { return (id: string, value: T) => { diff --git a/src/orders/components/OrderSettings/OrderSettings.tsx b/src/orders/components/OrderSettings/OrderSettings.tsx index 9d9ea12ef..72eea8d13 100644 --- a/src/orders/components/OrderSettings/OrderSettings.tsx +++ b/src/orders/components/OrderSettings/OrderSettings.tsx @@ -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; diff --git a/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx index 2a89aca03..288a379c0 100644 --- a/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx +++ b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx @@ -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; diff --git a/src/orders/components/OrderSettingsPage/form.tsx b/src/orders/components/OrderSettingsPage/form.tsx index db9f6896c..6c93d43ff 100644 --- a/src/orders/components/OrderSettingsPage/form.tsx +++ b/src/orders/components/OrderSettingsPage/form.tsx @@ -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; + submit: () => SubmitPromise; } 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, diff --git a/src/orders/components/OrderSettingsPage/types.ts b/src/orders/components/OrderSettingsPage/types.ts new file mode 100644 index 000000000..c158e647e --- /dev/null +++ b/src/orders/components/OrderSettingsPage/types.ts @@ -0,0 +1,6 @@ +export interface OrderSettingsFormData { + automaticallyConfirmAllNewOrders: boolean; + fulfillmentAutoApprove: boolean; + fulfillmentAllowUnpaid: boolean; + automaticallyFulfillNonShippableGiftCard: boolean; +} diff --git a/src/orders/views/OrderDetails/OrderDetails.tsx b/src/orders/views/OrderDetails/OrderDetails.tsx new file mode 100644 index 000000000..340a0723f --- /dev/null +++ b/src/orders/views/OrderDetails/OrderDetails.tsx @@ -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 = ({ 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 ( + + {({ data, loading }) => { + const order = data?.order; + if (order === null) { + return ; + } + + 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 ( + + {orderMessages => ( + { + 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 && ( + + )} + {isOrderDraft && ( + + )} + {isOrderUnconfirmed && ( + + )} + + + )} + + )} + + ); + }} + + ); +}; + +export default OrderDetails; diff --git a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx index 781343779..b4e4f737c 100644 --- a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx @@ -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 = ({ - orderAddNote.mutate({ - input: variables, - order: id - }) + extractMutationErrors( + orderAddNote.mutate({ + input: variables, + order: id + }) + ) } users={mapEdgesToItems(users?.data?.search)} hasMore={users?.data?.search?.pageInfo?.hasNextPage || false} @@ -245,13 +250,15 @@ export const OrderDraftDetails: React.FC = ({ onFetch={variantSearch} onFetchMore={loadMore} onSubmit={variants => - orderLinesAdd.mutate({ - id, - input: variants.map(variant => ({ - quantity: 1, - variantId: variant.id - })) - }) + extractMutationErrors( + orderLinesAdd.mutate({ + id, + input: variants.map(variant => ({ + quantity: 1, + variantId: variant.id + })) + }) + ) } /> = ({ updateMetadataOpts.loading || updatePrivateMetadataOpts.loading } onNoteAdd={variables => - orderAddNote.mutate({ - input: variables, - order: id - }) + extractMutationErrors( + orderAddNote.mutate({ + input: variables, + order: id + }) + ) } onBack={handleBack} order={order} diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index 809665745..5e184ab1c 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -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 = ( updateMetadataOpts.loading || updatePrivateMetadataOpts.loading } onNoteAdd={variables => - orderAddNote.mutate({ - input: variables, - order: id - }) + extractMutationErrors( + orderAddNote.mutate({ + input: variables, + order: id + }) + ) } onBack={handleBack} order={order} @@ -248,12 +254,14 @@ export const OrderUnconfirmedDetails: React.FC = ( shippingMethods={order?.shippingMethods} onClose={closeModal} onSubmit={variables => - orderShippingMethodUpdate.mutate({ - id, - input: { - shippingMethod: variables.shippingMethod - } - }) + extractMutationErrors( + orderShippingMethodUpdate.mutate({ + id, + input: { + shippingMethod: variables.shippingMethod + } + }) + ) } /> = ({ 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 ( - - {({ data, loading }) => { - const order = data?.order; - if (order === null) { - return ; - } - - 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 ( - - {orderMessages => ( - { - 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 && ( - - )} - {isOrderDraft && ( - - )} - {isOrderUnconfirmed && ( - - )} - - - )} - - )} - - ); - }} - - ); -}; - -export default OrderDetails; +export * from "./OrderDetails"; +export { default } from "./OrderDetails"; diff --git a/src/orders/views/OrderFulfill/OrderFulfill.tsx b/src/orders/views/OrderFulfill/OrderFulfill.tsx index 6f39648e7..a04443a23 100644 --- a/src/orders/views/OrderFulfill/OrderFulfill.tsx +++ b/src/orders/views/OrderFulfill/OrderFulfill.tsx @@ -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,19 +101,21 @@ const OrderFulfill: React.FC = ({ orderId }) => { errors={fulfillOrderOpts.data?.orderFulfill.errors} onBack={() => navigate(orderUrl(orderId))} onSubmit={formData => - fulfillOrder({ - variables: { - input: { - lines: formData.items.map(line => ({ - orderLineId: line.id, - stocks: line.value - })), - notifyCustomer: - settings?.shop?.fulfillmentAutoApprove && formData.sendInfo - }, - orderId - } - }) + extractMutationErrors( + fulfillOrder({ + variables: { + input: { + lines: formData.items.map(line => ({ + orderLineId: line.id, + stocks: line.value + })), + notifyCustomer: + settings?.shop?.fulfillmentAutoApprove && formData.sendInfo + }, + orderId + } + }) + ) } order={data?.order} saveButtonBar="default" diff --git a/src/orders/views/OrderRefund/OrderRefund.tsx b/src/orders/views/OrderRefund/OrderRefund.tsx index 4d9445efd..876f762cd 100644 --- a/src/orders/views/OrderRefund/OrderRefund.tsx +++ b/src/orders/views/OrderRefund/OrderRefund.tsx @@ -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 = ({ orderId }) => { const handleSubmitMiscellaneousRefund = async ( formData: OrderRefundSubmitData ) => { - const response = await refundOrder({ - variables: { - amount: formData.amount, - id: orderId - } - }); - - return response?.errors || []; + extractMutationErrors( + refundOrder({ + variables: { + amount: formData.amount, + id: orderId + } + }) + ); }; const handleSubmitProductsRefund = async ( @@ -120,14 +121,14 @@ const OrderRefund: React.FC = ({ orderId }) => { ? getAutomaticallyCalculatedProductsRefundInput(formData) : getManuallySetProductsRefundInput(formData); - const response = await refundOrderFulfillmentProducts({ - variables: { - input, - order: orderId - } - }); - - return response?.errors || []; + return extractMutationErrors( + refundOrderFulfillmentProducts({ + variables: { + input, + order: orderId + } + }) + ); }; const handleSubmit = async (formData: OrderRefundSubmitData) => diff --git a/src/orders/views/OrderReturn/OrderReturn.tsx b/src/orders/views/OrderReturn/OrderReturn.tsx index c27711dbf..9b27baa3e 100644 --- a/src/orders/views/OrderReturn/OrderReturn.tsx +++ b/src/orders/views/OrderReturn/OrderReturn.tsx @@ -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 = ({ orderId }) => { return; } - const result = await returnCreate({ - variables: { - id: data.order.id, - input: new ReturnFormDataParser(data.order, formData).getParsedData() - } - }); - - const { - data: { - orderFulfillmentReturnProducts = {} as FulfillmentReturnProducts_orderFulfillmentReturnProducts - } = {} - } = result || {}; - - return orderFulfillmentReturnProducts.errors; + return extractMutationErrors( + returnCreate({ + variables: { + id: data.order.id, + input: new ReturnFormDataParser(data.order, formData).getParsedData() + } + }) + ); }; const navigateToOrder = (id?: string) => navigate(orderUrl(id || orderId)); diff --git a/src/orders/views/OrderSettings.tsx b/src/orders/views/OrderSettings.tsx index fc29722a4..8b457b7b5 100644 --- a/src/orders/views/OrderSettings.tsx +++ b/src/orders/views/OrderSettings.tsx @@ -1,63 +1,65 @@ 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({ - variables: { - orderSettingsInput: { - automaticallyFulfillNonShippableGiftCard, - automaticallyConfirmAllNewOrders - }, - shopSettingsInput: { - fulfillmentAutoApprove, - fulfillmentAllowUnpaid + }: OrderSettingsFormData) => + extractMutationErrors( + orderSettingsUpdate({ + variables: { + orderSettingsInput: { + automaticallyFulfillNonShippableGiftCard, + automaticallyConfirmAllNewOrders + }, + shopSettingsInput: { + fulfillmentAutoApprove, + 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 ( = props => { } = useMetadataChangeTrigger(); return ( - + {({ change, data, hasChanged, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); diff --git a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx index 1769c738a..fd77150db 100644 --- a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx +++ b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx @@ -106,7 +106,7 @@ const PageTypeDetailsPage: React.FC = props => { }; return ( - + {({ change, data, hasChanged, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); diff --git a/src/pageTypes/views/PageTypeCreate.tsx b/src/pageTypes/views/PageTypeCreate.tsx index f1a056ac9..9827668f4 100644 --- a/src/pageTypes/views/PageTypeCreate.tsx +++ b/src/pageTypes/views/PageTypeCreate.tsx @@ -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, diff --git a/src/pages/components/PageDetailsPage/form.tsx b/src/pages/components/PageDetailsPage/form.tsx index de44c4afe..affa0b8bf 100644 --- a/src/pages/components/PageDetailsPage/form.tsx +++ b/src/pages/components/PageDetailsPage/form.tsx @@ -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 { 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([]); - const form = useForm({ - 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 }; } diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 497038238..32771cd17 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -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 = ({ params }) => { } }); - return result.data.pageCreate.page?.id || null; + return { + id: result.data.pageCreate.page?.id || null, + errors: getMutationErrors(result) + }; }; + const handleSubmit = createMetadataCreateHandler( handleCreate, updateMetadata, diff --git a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx index 854628f22..c087200ae 100644 --- a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx +++ b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx @@ -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 = ({ @@ -56,7 +57,7 @@ const PermissionGroupCreatePage: React.FC = ({ ); return ( - + {({ data, change, submit, hasChanged }) => ( diff --git a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx index a7ee3ddf6..a8a865fa9 100644 --- a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx +++ b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx @@ -90,7 +90,7 @@ const PermissionGroupDetailsPage: React.FC = ({ ); return ( - + {({ data, change, submit, hasChanged }) => ( diff --git a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx index c931c412c..c48e075d8 100644 --- a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx +++ b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx @@ -3,11 +3,14 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; +import { extractMutationErrors } from "@saleor/misc"; import { PermissionData } from "@saleor/permissionGroups/components/PermissionGroupDetailsPage"; import React from "react"; import { useIntl } from "react-intl"; -import PermissionGroupCreatePage from "../../components/PermissionGroupCreatePage"; +import PermissionGroupCreatePage, { + PermissionGroupCreateFormData +} from "../../components/PermissionGroupCreatePage"; import { usePermissionGroupCreate } from "../../mutations"; import { PermissionGroupCreate } from "../../types/PermissionGroupCreate"; import { permissionGroupDetailsUrl, permissionGroupListUrl } from "../../urls"; @@ -41,18 +44,20 @@ const PermissionGroupCreateView: React.FC = () => { const errors = createPermissionGroupResult?.data?.permissionGroupCreate?.errors || []; - const onSubmit = formData => - createPermissionGroup({ - variables: { - input: { - addPermissions: formData.hasFullAccess - ? shop.permissions.map(perm => perm.code) - : formData.permissions, - addUsers: [], - name: formData.name + const onSubmit = (formData: PermissionGroupCreateFormData) => + extractMutationErrors( + createPermissionGroup({ + variables: { + input: { + addPermissions: formData.hasFullAccess + ? shop.permissions.map(perm => perm.code) + : formData.permissions, + addUsers: [], + name: formData.name + } } - } - }); + }) + ); const userPermissions = user?.user.userPermissions.map(p => p.code) || []; diff --git a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx index 3ea5cc850..a6d07721c 100644 --- a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx +++ b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx @@ -7,6 +7,7 @@ import useShop from "@saleor/hooks/useShop"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { commonMessages } from "@saleor/intl"; import { Button } from "@saleor/macaw-ui"; +import { extractMutationErrors } from "@saleor/misc"; import MembersErrorDialog from "@saleor/permissionGroups/components/MembersErrorDialog"; import { arePermissionsExceeded, @@ -134,20 +135,19 @@ export const PermissionGroupDetails: React.FC = ({ ); const disabled = loading || !isGroupEditable || permissionsExceeded; - const handleSubmit = async (formData: PermissionGroupDetailsPageFormData) => { - const result = await permissionGroupUpdate({ - variables: { - id, - input: { - name: formData.name, - ...permissionsDiff(data?.permissionGroup, formData), - ...usersDiff(data?.permissionGroup, formData) + const handleSubmit = async (formData: PermissionGroupDetailsPageFormData) => + extractMutationErrors( + permissionGroupUpdate({ + variables: { + id, + input: { + name: formData.name, + ...permissionsDiff(data?.permissionGroup, formData), + ...usersDiff(data?.permissionGroup, formData) + } } - } - }); - - return result.data.permissionGroupUpdate.errors; - }; + }) + ); return ( <> diff --git a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx index 73ef33b9a..bbe1873a4 100644 --- a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx +++ b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx @@ -5,7 +5,7 @@ import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; import { PluginErrorFragment } from "@saleor/fragments/types/PluginErrorFragment"; -import { ChangeEvent } from "@saleor/hooks/useForm"; +import { ChangeEvent, SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui"; @@ -35,7 +35,7 @@ export interface PluginsDetailsPageProps { onBack: () => void; onClear: (field: string) => void; onEdit: (field: string) => void; - onSubmit: (data: PluginDetailsPageFormData) => void; + onSubmit: (data: PluginDetailsPageFormData) => SubmitPromise; selectedConfig?: PluginConfiguration; setSelectedChannelId: (channelId: string) => void; } @@ -70,6 +70,7 @@ const PluginsDetailsPage: React.FC = ({ return ( = ({ } }); - const handleSubmit = async (formData: PluginDetailsPageFormData) => { - const result = await pluginUpdate({ - variables: { - channelId: selectedChannelId, - id, - input: { - active: formData.active, - configuration: getConfigurationInput( - selectedConfig?.configuration, - formData.configuration - ) + const handleSubmit = async (formData: PluginDetailsPageFormData) => + extractMutationErrors( + pluginUpdate({ + variables: { + channelId: selectedChannelId, + id, + input: { + active: formData.active, + configuration: getConfigurationInput( + selectedConfig?.configuration, + formData.configuration + ) + } } - } - }); - - return result.data.pluginUpdate.errors; - }; + }) + ); return ( <> diff --git a/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx b/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx index b51e6a138..c3754ac01 100644 --- a/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx +++ b/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx @@ -5,7 +5,7 @@ import Grid from "@saleor/components/Grid"; import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; -import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; +import { ChangeEvent, FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; @@ -43,7 +43,7 @@ export interface ProductTypeCreatePageProps { kind: ProductTypeKindEnum; onChangeKind: (kind: ProductTypeKindEnum) => void; onBack: () => void; - onSubmit: (data: ProductTypeForm) => void; + onSubmit: (data: ProductTypeForm) => SubmitPromise; } const formInitialData: ProductTypeForm = { @@ -92,7 +92,7 @@ const ProductTypeCreatePage: React.FC = ({ }; return ( - + {({ change, data, hasChanged, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); diff --git a/src/productTypes/views/ProductTypeCreate.tsx b/src/productTypes/views/ProductTypeCreate.tsx index 3964118eb..fe15a51f3 100644 --- a/src/productTypes/views/ProductTypeCreate.tsx +++ b/src/productTypes/views/ProductTypeCreate.tsx @@ -10,7 +10,7 @@ import { import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../misc"; +import { getMutationErrors, maybe } from "../../misc"; import ProductTypeCreatePage, { ProductTypeForm } from "../components/ProductTypeCreatePage"; @@ -74,8 +74,12 @@ export const ProductTypeCreate: React.FC = ({ } }); - return result.data?.productTypeCreate.productType?.id || null; + return { + id: result.data?.productTypeCreate.productType?.id || null, + errors: getMutationErrors(result) + }; }; + const handleSubmit = createMetadataCreateHandler( handleCreate, updateMetadata, diff --git a/src/products/components/ProductCreatePage/form.tsx b/src/products/components/ProductCreatePage/form.tsx index ce0d9f643..1edba0bc9 100644 --- a/src/products/components/ProductCreatePage/form.tsx +++ b/src/products/components/ProductCreatePage/form.tsx @@ -14,15 +14,22 @@ import { AttributeInput, AttributeInputData } from "@saleor/components/Attributes"; +import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import { MetadataFormData } from "@saleor/components/Metadata"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; -import useForm, { FormChange, FormErrors } from "@saleor/hooks/useForm"; +import useForm, { + CommonUseFormResultWithHandlers, + FormChange, + FormErrors, + SubmitPromise +} from "@saleor/hooks/useForm"; import useFormset, { FormsetChange, FormsetData } from "@saleor/hooks/useFormset"; +import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; import { errorMessages } from "@saleor/intl"; import { ProductType_productType } from "@saleor/products/types/ProductType"; import { @@ -38,6 +45,7 @@ import { validateCostPrice, validatePrice } from "@saleor/products/utils/validation"; +import { PRODUCT_CREATE_FORM_ID } from "@saleor/products/views/ProductCreate/consts"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes"; @@ -47,7 +55,7 @@ import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAu import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useRichText from "@saleor/utils/richText/useRichText"; -import React from "react"; +import React, { useEffect } from "react"; import { useIntl } from "react-intl"; import { createPreorderEndDateChangeHandler } from "../../utils/handlers"; @@ -114,14 +122,13 @@ export interface ProductCreateHandlers fetchReferences: (value: string) => void; fetchMoreReferences: FetchMoreProps; } -export interface UseProductCreateFormResult { - change: FormChange; - data: ProductCreateData; - formErrors: FormErrors; +export interface UseProductCreateFormResult + extends CommonUseFormResultWithHandlers< + ProductCreateData, + ProductCreateHandlers + > { disabled: boolean; - handlers: ProductCreateHandlers; - hasChanged: boolean; - submit: () => Promise; + formErrors: FormErrors; } export interface UseProductCreateFormOpts @@ -153,12 +160,12 @@ export interface UseProductCreateFormOpts export interface ProductCreateFormProps extends UseProductCreateFormOpts { children: (props: UseProductCreateFormResult) => React.ReactNode; initial?: Partial; - onSubmit: (data: ProductCreateData) => Promise; + onSubmit: (data: ProductCreateData) => SubmitPromise; } function useProductCreateForm( initial: Partial, - onSubmit: (data: ProductCreateData) => Promise, + onSubmit: (data: ProductCreateData) => SubmitPromise, opts: UseProductCreateFormOpts ): UseProductCreateFormResult { const intl = useIntl(); @@ -190,13 +197,26 @@ function useProductCreateForm( hasPreorderEndDate: false, preorderEndDateTime: "" }; - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); - const form = useForm({ - ...initial, - ...defaultInitialFormData - }); + const form = useForm( + { + ...initial, + ...defaultInitialFormData + }, + undefined, + { confirmLeave: true, formId: PRODUCT_CREATE_FORM_ID } + ); + + const { + triggerChange, + toggleValue, + handleChange, + hasChanged, + data: formData, + setChanged, + formId + } = form; + const attributes = useFormset( opts.selectedProductType ? getAttributeInputFromProductType(opts.selectedProductType) @@ -209,16 +229,16 @@ function useProductCreateForm( triggerChange }); + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId: PRODUCT_CREATE_FORM_ID + }); + const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); - const handleChange: FormChange = (event, cb) => { - form.change(event, cb); - triggerChange(); - }; const handleCollectionSelect = createMultiAutocompleteSelectHandler( - form.toggleValue, + toggleValue, opts.setSelectedCollections, opts.selectedCollections, opts.collections @@ -312,7 +332,7 @@ function useProductCreateForm( ); const getData = (): ProductCreateData => ({ - ...form.data, + ...formData, attributes: getAttributesDisplayData( attributes.data, attributesWithNewFileValue.data, @@ -324,8 +344,18 @@ function useProductCreateForm( productType: opts.selectedProductType, stocks: stocks.data }); + const data = getData(); - const submit = () => onSubmit(data); + + const handleFormSubmit = useHandleFormSubmit({ + formId, + onSubmit, + setChanged + }); + + const submit = () => handleFormSubmit(data); + + useEffect(() => setExitDialogSubmitRef(submit), [submit]); const shouldEnableSave = () => { if (!data.name || !data.productType) { @@ -383,7 +413,7 @@ function useProductCreateForm( selectProductType: handleProductTypeSelect, selectTaxRate: handleTaxTypeSelect }, - hasChanged: changed, + hasChanged, submit }; } diff --git a/src/products/components/ProductExternalMediaDialog/ProductExternalMediaDialog.tsx b/src/products/components/ProductExternalMediaDialog/ProductExternalMediaDialog.tsx index 2b7b1a9f0..fb21010e6 100644 --- a/src/products/components/ProductExternalMediaDialog/ProductExternalMediaDialog.tsx +++ b/src/products/components/ProductExternalMediaDialog/ProductExternalMediaDialog.tsx @@ -9,6 +9,7 @@ import { import BackButton from "@saleor/components/BackButton"; 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 { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import React from "react"; @@ -18,7 +19,7 @@ interface ProductExternalMediaDialogProps { product: ProductDetails_product; open: boolean; onClose: () => void; - onSubmit: (mediaUrl: string) => void; + onSubmit: (mediaUrl: string) => SubmitPromise; } interface FormValues { diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index abcfc33c1..a3d96fcac 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -15,11 +15,13 @@ import { ChannelPriceArgs } from "@saleor/channels/utils"; import { AttributeInput } from "@saleor/components/Attributes"; +import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import { MetadataFormData } from "@saleor/components/Metadata"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import useForm, { + CommonUseFormResultWithHandlers, FormChange, FormErrors, SubmitPromise @@ -29,6 +31,7 @@ import useFormset, { FormsetChange, FormsetData } from "@saleor/hooks/useFormset"; +import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; import { errorMessages } from "@saleor/intl"; import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import { @@ -46,19 +49,19 @@ import { validateCostPrice, validatePrice } from "@saleor/products/utils/validation"; +import { PRODUCT_UPDATE_FORM_ID } from "@saleor/products/views/ProductUpdate/consts"; import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { FetchMoreProps, ReorderEvent } from "@saleor/types"; import { arrayDiff } from "@saleor/utils/arrays"; -import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; 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"; import { useIntl } from "react-intl"; import { ProductStockFormsetData, ProductStockInput } from "../ProductStocks"; @@ -147,14 +150,13 @@ export interface ProductUpdateHandlers fetchReferences: (value: string) => void; fetchMoreReferences: FetchMoreProps; } -export interface UseProductUpdateFormResult { - change: FormChange; - data: ProductUpdateData; - formErrors: FormErrors; +export interface UseProductUpdateFormResult + extends CommonUseFormResultWithHandlers< + ProductUpdateData, + ProductUpdateHandlers + > { disabled: boolean; - handlers: ProductUpdateHandlers; - hasChanged: boolean; - submit: () => Promise; + formErrors: FormErrors; } export interface UseProductUpdateFormOpts @@ -221,8 +223,6 @@ function useProductUpdateForm( opts: UseProductUpdateFormOpts ): UseProductUpdateFormResult { const intl = useIntl(); - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); const form = useForm( getProductUpdatePageFormData( @@ -231,8 +231,20 @@ function useProductUpdateForm( opts.currentChannels, opts.channelsData, opts.channelsWithVariants - ) + ), + undefined, + { confirmLeave: true, formId: PRODUCT_UPDATE_FORM_ID } ); + + const { + handleChange, + triggerChange, + toggleValue, + data: formData, + setChanged, + hasChanged + } = form; + const attributes = useFormset(getAttributeInputFromProduct(product)); const attributesWithNewFileValue = useFormset([]); const stocks = useFormset(getStockInputFromProduct(product)); @@ -241,18 +253,18 @@ function useProductUpdateForm( triggerChange }); + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId: PRODUCT_UPDATE_FORM_ID + }); + const { isMetadataModified, isPrivateMetadataModified, makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); - const handleChange: FormChange = (event, cb) => { - form.change(event, cb); - triggerChange(); - }; const handleCollectionSelect = createMultiAutocompleteSelectHandler( - event => form.toggleValue(event, triggerChange), + event => toggleValue(event), opts.setSelectedCollections, opts.selectedCollections, opts.collections @@ -350,7 +362,7 @@ function useProductUpdateForm( ); const data: ProductUpdateData = { - ...form.data, + ...formData, channelListings: opts.currentChannels, channelsData: opts.channelsData, attributes: getAttributesDisplayData( @@ -383,8 +395,15 @@ function useProductUpdateForm( return errors; }; - const submit = async () => - handleFormSubmit(getSubmitData(), handleSubmit, setChanged); + const handleFormSubmit = useHandleFormSubmit({ + formId: form.formId, + onSubmit: handleSubmit, + setChanged + }); + + const submit = async () => handleFormSubmit(getSubmitData()); + + useEffect(() => setExitDialogSubmitRef(submit), [submit]); const shouldEnableSave = () => { if (!data.name) { @@ -442,7 +461,7 @@ function useProductUpdateForm( selectCollection: handleCollectionSelect, selectTaxRate: handleTaxTypeSelect }, - hasChanged: changed, + hasChanged, submit }; } diff --git a/src/products/components/ProductVariantCreatePage/form.tsx b/src/products/components/ProductVariantCreatePage/form.tsx index 93a209e9f..e398c978c 100644 --- a/src/products/components/ProductVariantCreatePage/form.tsx +++ b/src/products/components/ProductVariantCreatePage/form.tsx @@ -9,12 +9,18 @@ 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 useForm, { FormChange, FormErrors } from "@saleor/hooks/useForm"; +import useForm, { + CommonUseFormResultWithHandlers, + FormChange, + FormErrors +} from "@saleor/hooks/useForm"; import useFormset, { FormsetChange, FormsetData } from "@saleor/hooks/useFormset"; +import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; import { errorMessages } from "@saleor/intl"; import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData"; import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data"; @@ -24,7 +30,7 @@ import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchP import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { FetchMoreProps, ReorderEvent } from "@saleor/types"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import React from "react"; +import React, { useEffect } from "react"; import { useIntl } from "react-intl"; import { ProductStockFormsetData, ProductStockInput } from "../ProductStocks"; @@ -72,15 +78,13 @@ export interface ProductVariantCreateHandlers fetchMoreReferences: FetchMoreProps; } -export interface UseProductVariantCreateFormResult { - change: FormChange; - data: ProductVariantCreateData; +export interface UseProductVariantCreateFormResult + extends CommonUseFormResultWithHandlers< + ProductVariantCreateData, + ProductVariantCreateHandlers + > { formErrors: FormErrors; disabled: boolean; - // TODO: type FormsetChange - handlers: ProductVariantCreateHandlers; - hasChanged: boolean; - submit: () => void; } export interface ProductVariantCreateFormProps @@ -110,23 +114,31 @@ function useProductVariantCreateForm( opts: UseProductVariantCreateFormOpts ): UseProductVariantCreateFormResult { const intl = useIntl(); - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); - const attributeInput = getVariantAttributeInputFromProduct(product); - const form = useForm(initial); + const form = useForm(initial, undefined, { confirmLeave: true }); + + const { + setChanged, + triggerChange, + handleChange, + hasChanged, + data: formData, + formId + } = form; + const attributes = useFormset(attributeInput); const attributesWithNewFileValue = useFormset([]); const stocks = useFormset([]); + + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId + }); + const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); - const handleChange: FormChange = (event, cb) => { - form.change(event, cb); - triggerChange(); - }; const changeMetadata = makeMetadataChangeHandler(handleChange); const handleAttributeChange = createAttributeChangeHandler( attributes.change, @@ -192,7 +204,7 @@ function useProductVariantCreateForm( ); const data: ProductVariantCreateData = { - ...form.data, + ...formData, attributes: getAttributesDisplayData( attributes.data, attributesWithNewFileValue.data, @@ -203,7 +215,15 @@ function useProductVariantCreateForm( stocks: stocks.data }; - const submit = () => onSubmit(data); + const handleFormSubmit = useHandleFormSubmit({ + formId, + onSubmit, + setChanged + }); + + const submit = () => handleFormSubmit(data); + + useEffect(() => setExitDialogSubmitRef(submit), [submit]); return { change: handleChange, @@ -227,7 +247,7 @@ function useProductVariantCreateForm( selectAttributeMultiple: handleAttributeMultiChange, selectAttributeReference: handleAttributeReferenceChange }, - hasChanged: changed, + hasChanged, submit }; } diff --git a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx index 237b5fd4a..3f967bce4 100644 --- a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx +++ b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx @@ -3,6 +3,7 @@ import Container from "@saleor/components/Container"; import Hr from "@saleor/components/Hr"; import PageHeader from "@saleor/components/PageHeader"; import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import useWizard from "@saleor/hooks/useWizard"; import { Button, makeStyles } from "@saleor/macaw-ui"; import { validatePrice } from "@saleor/products/utils/validation"; @@ -91,7 +92,7 @@ export interface ProductVariantCreatePageProps "data" | "dispatchFormDataAction" | "step" | "variantsLeft" | "onStepClick" > { limits: RefreshLimits_shop_limits; - onSubmit: (data: ProductVariantBulkCreateInput[]) => void; + onSubmit: (data: ProductVariantBulkCreateInput[]) => SubmitPromise; } function getTitle(step: ProductVariantCreatorStep, intl: IntlShape): string { diff --git a/src/products/components/ProductVariantPage/form.tsx b/src/products/components/ProductVariantPage/form.tsx index 614bb9d2c..00d097018 100644 --- a/src/products/components/ProductVariantPage/form.tsx +++ b/src/products/components/ProductVariantPage/form.tsx @@ -13,9 +13,11 @@ import { IChannelPriceAndPreorderArgs } from "@saleor/channels/utils"; import { AttributeInput } from "@saleor/components/Attributes"; +import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import { MetadataFormData } from "@saleor/components/Metadata"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import useForm, { + CommonUseFormResultWithHandlers, FormChange, FormErrors, SubmitPromise @@ -24,6 +26,7 @@ import useFormset, { FormsetChange, FormsetData } from "@saleor/hooks/useFormset"; +import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; import { errorMessages } from "@saleor/intl"; import { getAttributeInputFromVariant, @@ -45,10 +48,9 @@ import { arrayDiff } from "@saleor/utils/arrays"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; import getMetadata from "@saleor/utils/metadata/getMetadata"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import React from "react"; +import React, { useEffect } from "react"; import { useIntl } from "react-intl"; -import handleFormSubmit from "../../../utils/handlers/handleFormSubmit"; import { ProductStockInput } from "../ProductStocks"; export interface ProductVariantUpdateFormData extends MetadataFormData { @@ -113,14 +115,13 @@ export interface ProductVariantUpdateHandlers fetchMoreReferences: FetchMoreProps; } -export interface UseProductVariantUpdateFormResult { - change: FormChange; - data: ProductVariantUpdateData; +export interface UseProductVariantUpdateFormResult + extends CommonUseFormResultWithHandlers< + ProductVariantUpdateData, + ProductVariantUpdateHandlers + > { formErrors: FormErrors; disabled: boolean; - handlers: ProductVariantUpdateHandlers; - hasChanged: boolean; - submit: () => void; } export interface ProductVariantUpdateFormProps @@ -136,9 +137,6 @@ function useProductVariantUpdateForm( opts: UseProductVariantUpdateFormOpts ): UseProductVariantUpdateFormResult { const intl = useIntl(); - const [changed, setChanged] = React.useState(false); - const triggerChange = () => setChanged(true); - const attributeInput = getAttributeInputFromVariant(variant); const stockInput = getStockInputFromVariant(variant); @@ -170,7 +168,23 @@ function useProductVariantUpdateForm( quantityLimitPerCustomer: variant?.quantityLimitPerCustomer || null }; - const form = useForm(initial); + const form = useForm(initial, undefined, { + confirmLeave: true + }); + + const { + handleChange, + triggerChange, + data: formData, + setChanged, + hasChanged, + formId + } = form; + + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId + }); + const attributes = useFormset(attributeInput); const attributesWithNewFileValue = useFormset([]); const stocks = useFormset(stockInput); @@ -181,10 +195,6 @@ function useProductVariantUpdateForm( makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); - const handleChange: FormChange = (event, cb) => { - form.change(event, cb); - triggerChange(); - }; const changeMetadata = makeMetadataChangeHandler(handleChange); const handleAttributeChange = createAttributeChangeHandler( attributes.change, @@ -268,7 +278,7 @@ function useProductVariantUpdateForm( ); const data: ProductVariantUpdateData = { - ...form.data, + ...formData, attributes: getAttributesDisplayData( attributes.data, attributesWithNewFileValue.data, @@ -290,8 +300,8 @@ function useProductVariantUpdateForm( !!form.errors.preorderEndDateTime); const submitData: ProductVariantUpdateSubmitData = { - ...form.data, - ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + ...formData, + ...getMetadata(formData, isMetadataModified, isPrivateMetadataModified), addStocks, attributes: attributes.data, attributesWithNewFileValue: attributesWithNewFileValue.data, @@ -310,7 +320,15 @@ function useProductVariantUpdateForm( return errors; }; - const submit = () => handleFormSubmit(submitData, handleSubmit, setChanged); + const handleFormSubmit = useHandleFormSubmit({ + formId, + onSubmit: handleSubmit, + setChanged + }); + + const submit = () => handleFormSubmit(submitData); + + useEffect(() => setExitDialogSubmitRef(submit), [submit]); return { change: handleChange, @@ -332,7 +350,7 @@ function useProductVariantUpdateForm( selectAttributeMultiple: handleAttributeMultiChange, selectAttributeReference: handleAttributeReferenceChange }, - hasChanged: changed, + hasChanged, submit }; } diff --git a/src/products/utils/handlers.ts b/src/products/utils/handlers.ts index efc8993eb..8149546c9 100644 --- a/src/products/utils/handlers.ts +++ b/src/products/utils/handlers.ts @@ -145,8 +145,8 @@ export const createPreorderEndDateChangeHandler = ( form: UseFormResult<{ preorderEndDateTime?: string }>, triggerChange: () => void, preorderPastDateErrorMessage: string -): FormChange => (event, cb) => { - form.change(event, cb); +): FormChange => event => { + form.change(event); if (moment(event.target.value).isSameOrBefore(Date.now())) { form.setError("preorderEndDateTime", preorderPastDateErrorMessage); } else { diff --git a/src/products/views/ProductCreate/ProductCreate.tsx b/src/products/views/ProductCreate/ProductCreate.tsx index e6826cef7..9e04bcacc 100644 --- a/src/products/views/ProductCreate/ProductCreate.tsx +++ b/src/products/views/ProductCreate/ProductCreate.tsx @@ -12,7 +12,9 @@ import useChannels from "@saleor/hooks/useChannels"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; -import ProductCreatePage from "@saleor/products/components/ProductCreatePage"; +import ProductCreatePage, { + ProductCreateData +} from "@saleor/products/components/ProductCreatePage"; import { useProductChannelListingUpdate, useProductDeleteMutation, @@ -48,6 +50,7 @@ import { warehouseAddPath } from "@saleor/warehouses/urls"; import React from "react"; import { useIntl } from "react-intl"; +import { PRODUCT_CREATE_FORM_ID } from "./consts"; import { createHandler } from "./handlers"; interface ProductCreateProps { @@ -148,10 +151,17 @@ export const ProductCreateView: React.FC = ({ params }) => { isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(allChannels, params?.action, { - closeModal, - openModal - }); + } = useChannels( + allChannels, + params?.action, + { + closeModal, + openModal + }, + { + formId: PRODUCT_CREATE_FORM_ID + } + ); const handleSuccess = (productId: string) => { notify({ @@ -194,10 +204,10 @@ export const ProductCreateView: React.FC = ({ params }) => { } }); - const handleSubmit = async data => { - const result = await createMetadataCreateHandler( + const handleSubmit = async (data: ProductCreateData) => { + const errors = await createMetadataCreateHandler( createHandler( - selectedProductType.productType, + selectedProductType?.productType, variables => uploadFile({ variables }), variables => productCreate({ variables }), variables => productVariantCreate({ variables }), @@ -209,9 +219,11 @@ export const ProductCreateView: React.FC = ({ params }) => { updatePrivateMetadata )(data); - if (result) { + if (!errors?.length) { setProductCreateComplete(true); } + + return errors; }; const handleAssignAttributeReferenceClick = (attribute: AttributeInput) => diff --git a/src/products/views/ProductCreate/consts.ts b/src/products/views/ProductCreate/consts.ts new file mode 100644 index 000000000..d5b644c98 --- /dev/null +++ b/src/products/views/ProductCreate/consts.ts @@ -0,0 +1 @@ +export const PRODUCT_CREATE_FORM_ID = Symbol(); diff --git a/src/products/views/ProductCreate/handlers.ts b/src/products/views/ProductCreate/handlers.ts index b32e80067..a7beb01c8 100644 --- a/src/products/views/ProductCreate/handlers.ts +++ b/src/products/views/ProductCreate/handlers.ts @@ -138,7 +138,7 @@ export function createHandler( const productId = result.data.productCreate.product?.id; if (!productId) { - return null; + return { errors }; } if (!hasVariants) { @@ -185,8 +185,8 @@ export function createHandler( if (productId && hasErrors) { await productDelete({ variables: { id: productId } }); - return null; + return { errors }; } - return productId || null; + return { id: productId || null, errors }; }; } diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 909e47e3b..63851833d 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -79,6 +79,7 @@ import { productVariantEditUrl } from "../../urls"; import { CHANNELS_AVAILIABILITY_MODAL_SELECTOR } from "./consts"; +import { PRODUCT_UPDATE_FORM_ID } from "./consts"; import { createImageReorderHandler, createImageUploadHandler, @@ -304,10 +305,15 @@ export const ProductUpdate: React.FC = ({ id, params }) => { isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(productChannelsChoices, params?.action, { - closeModal, - openModal - }); + } = useChannels( + productChannelsChoices, + params?.action, + { + closeModal, + openModal + }, + { formId: PRODUCT_UPDATE_FORM_ID } + ); const [updateChannels, updateChannelsOpts] = useProductChannelListingUpdate({ onCompleted: data => { diff --git a/src/products/views/ProductUpdate/consts.ts b/src/products/views/ProductUpdate/consts.ts index bf243dcf7..60af0ec27 100644 --- a/src/products/views/ProductUpdate/consts.ts +++ b/src/products/views/ProductUpdate/consts.ts @@ -7,3 +7,5 @@ export const initialChannelWithVariantData: ChannelWithVariantData = { variantsIdsToAdd: [], selectedVariantsIds: [] }; + +export const PRODUCT_UPDATE_FORM_ID = Symbol(); diff --git a/src/products/views/ProductVariant.tsx b/src/products/views/ProductVariant.tsx index 1e9b16648..9f453c757 100644 --- a/src/products/views/ProductVariant.tsx +++ b/src/products/views/ProductVariant.tsx @@ -41,7 +41,7 @@ import { warehouseAddPath } from "@saleor/warehouses/urls"; import React, { useEffect, useState } from "react"; import { useIntl } from "react-intl"; -import { weight } from "../../misc"; +import { extractMutationErrors, weight } from "../../misc"; import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog"; import ProductVariantPage from "../components/ProductVariantPage"; import { ProductVariantUpdateSubmitData } from "../components/ProductVariantPage/form"; @@ -180,17 +180,19 @@ export const ProductVariant: React.FC = ({ }); if (channelsHaveChanged) { - await updateChannels({ - variables: { - id: variant.id, - input: data.channelListings.map(listing => ({ - channelId: listing.id, - costPrice: listing.value.costPrice || null, - price: listing.value.price, - preorderThreshold: listing.value.preorderThreshold - })) - } - }); + return extractMutationErrors( + updateChannels({ + variables: { + id: variant.id, + input: data.channelListings.map(listing => ({ + channelId: listing.id, + costPrice: listing.value.costPrice || null, + price: listing.value.price, + preorderThreshold: listing.value.preorderThreshold + })) + } + }) + ); } }; @@ -385,8 +387,9 @@ export const ProductVariant: React.FC = ({ onDelete={() => openModal("remove")} onMediaSelect={handleMediaSelect} onSubmit={async data => { - await handleSubmit(data); - await handleSubmitChannels(data, variant); + const errors = await handleSubmit(data); + const channelErrors = await handleSubmitChannels(data, variant); + return [...errors, ...channelErrors]; }} onWarehouseConfigure={() => navigate(warehouseAddPath)} onVariantClick={variantId => { diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index a46857ff7..baa7865ef 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -25,7 +25,7 @@ import { warehouseAddPath } from "@saleor/warehouses/urls"; import React from "react"; import { useIntl } from "react-intl"; -import { weight } from "../../misc"; +import { getMutationErrors, weight } from "../../misc"; import ProductVariantCreatePage from "../components/ProductVariantCreatePage"; import { ProductVariantCreateData } from "../components/ProductVariantCreatePage/form"; import { @@ -152,10 +152,11 @@ export const ProductVariant: React.FC = ({ firstValues: 10 } }); - const id = result.data?.productVariantCreate?.productVariant?.id; + const id = result.data?.productVariantCreate?.productVariant?.id || null; - return id || null; + return { id, errors: getMutationErrors(result) }; }; + const handleSubmit = createMetadataCreateHandler( handleCreate, updateMetadata, diff --git a/src/shipping/components/ShippingWeightUnitForm/ShippingWeightUnitForm.tsx b/src/shipping/components/ShippingWeightUnitForm/ShippingWeightUnitForm.tsx index 6a7809798..14fb30db1 100644 --- a/src/shipping/components/ShippingWeightUnitForm/ShippingWeightUnitForm.tsx +++ b/src/shipping/components/ShippingWeightUnitForm/ShippingWeightUnitForm.tsx @@ -2,6 +2,7 @@ import { Card, CardActions, CardContent } from "@material-ui/core"; import CardTitle from "@saleor/components/CardTitle"; import Form from "@saleor/components/Form"; import SingleSelectField from "@saleor/components/SingleSelectField"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { buttonMessages, sectionNames } from "@saleor/intl"; import { Button } from "@saleor/macaw-ui"; import React from "react"; @@ -16,7 +17,7 @@ export interface FormData { export interface ShippingWeightUnitFormProps { defaultWeightUnit: WeightUnitsEnum; disabled: boolean; - onSubmit: (unit: WeightUnitsEnum) => void; + onSubmit: (unit: WeightUnitsEnum) => SubmitPromise; } const ShippingWeightUnitForm: React.FC = ({ @@ -29,7 +30,11 @@ const ShippingWeightUnitForm: React.FC = ({ unit: defaultWeightUnit }; return ( - onSubmit(formData.unit)}> + onSubmit(formData.unit)} + > {({ change, data, submit }) => ( diff --git a/src/shipping/components/ShippingZoneAddWarehouseDialog/ShippingZoneAddWarehouseDialog.tsx b/src/shipping/components/ShippingZoneAddWarehouseDialog/ShippingZoneAddWarehouseDialog.tsx index c80d4538e..68c747ea0 100644 --- a/src/shipping/components/ShippingZoneAddWarehouseDialog/ShippingZoneAddWarehouseDialog.tsx +++ b/src/shipping/components/ShippingZoneAddWarehouseDialog/ShippingZoneAddWarehouseDialog.tsx @@ -15,6 +15,7 @@ import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo" import { AddressTypeInput } from "@saleor/customers/types"; import { WarehouseErrorFragment } from "@saleor/fragments/types/WarehouseErrorFragment"; import useAddressValidation from "@saleor/hooks/useAddressValidation"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; @@ -35,7 +36,7 @@ export interface ShippingZoneAddWarehouseDialogProps extends DialogProps { countries: ShopInfo_shop_countries[]; disabled: boolean; errors: WarehouseErrorFragment[]; - onSubmit: (data: ShippingZoneAddWarehouseDialogSubmitData) => void; + onSubmit: (data: ShippingZoneAddWarehouseDialogSubmitData) => SubmitPromise; } const initialForm: ShippingZoneAddWarehouseDialogSubmitData = { diff --git a/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.tsx b/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.tsx index 41f8df714..aaeda6ac5 100644 --- a/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.tsx +++ b/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.tsx @@ -92,7 +92,7 @@ const ShippingZoneCountriesAssignDialog: React.FC change(event, () => fetch(data.query))} + onChange={event => change(event)} label={intl.formatMessage({ defaultMessage: "Search Countries" })} diff --git a/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx b/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx index 501c82f7e..1516469c5 100644 --- a/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx +++ b/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx @@ -7,6 +7,7 @@ import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; import { CountryFragment } from "@saleor/fragments/types/CountryFragment"; import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui"; @@ -16,7 +17,7 @@ import { defineMessages, useIntl } from "react-intl"; import ShippingZoneCountriesAssignDialog from "../ShippingZoneCountriesAssignDialog"; import ShippingZoneInfo from "../ShippingZoneInfo"; -export interface FormData { +export interface ShippingZoneCreateFormData { countries: string[]; default: boolean; description: string; @@ -48,7 +49,7 @@ export interface ShippingZoneCreatePageProps { errors: ShippingErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: ShippingZoneCreateFormData) => SubmitPromise; } const ShippingZoneCreatePage: React.FC = ({ @@ -63,7 +64,7 @@ const ShippingZoneCreatePage: React.FC = ({ const [isModalOpened, setModalStatus] = React.useState(false); const toggleModal = () => setModalStatus(!isModalOpened); - const initialForm: FormData = { + const initialForm: ShippingZoneCreateFormData = { countries: [], default: false, description: "", @@ -71,7 +72,7 @@ const ShippingZoneCreatePage: React.FC = ({ }; return ( - + {({ change, data, hasChanged, submit }) => ( <> diff --git a/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx b/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx index 2c258986e..964168caf 100644 --- a/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx +++ b/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx @@ -25,7 +25,7 @@ import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { getStringOrPlaceholder } from "../../../misc"; import { ChannelProps, FetchMoreProps, SearchProps } from "../../../types"; import { ShippingMethodTypeEnum } from "../../../types/globalTypes"; -import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import { ShippingZoneUpdateFormData } from "../../components/ShippingZoneDetailsPage/types"; import ShippingZoneInfo from "../ShippingZoneInfo"; import ShippingZoneRates from "../ShippingZoneRates"; import ShippingZoneSettingsCard from "../ShippingZoneSettingsCard"; @@ -66,7 +66,7 @@ export interface ShippingZoneDetailsPageProps onPriceRateAdd: () => void; onPriceRateEdit: (id: string) => void; onRateRemove: (rateId: string) => void; - onSubmit: (data: FormData) => SubmitPromise; + onSubmit: (data: ShippingZoneUpdateFormData) => SubmitPromise; onWarehouseAdd: () => void; onWeightRateAdd: () => void; onWeightRateEdit: (id: string) => void; @@ -127,7 +127,7 @@ const ShippingZoneDetailsPage: React.FC = ({ } = useMetadataChangeTrigger(); return ( - + {({ change, data, hasChanged, submit, toggleValue }) => { const handleWarehouseChange = createMultiAutocompleteSelectHandler( toggleValue, diff --git a/src/shipping/components/ShippingZoneDetailsPage/types.ts b/src/shipping/components/ShippingZoneDetailsPage/types.ts index 0b0e50515..9bbf022e2 100644 --- a/src/shipping/components/ShippingZoneDetailsPage/types.ts +++ b/src/shipping/components/ShippingZoneDetailsPage/types.ts @@ -1,6 +1,6 @@ import { MetadataFormData } from "@saleor/components/Metadata"; -export interface FormData extends MetadataFormData { +export interface ShippingZoneUpdateFormData extends MetadataFormData { name: string; description: string; warehouses: string[]; diff --git a/src/shipping/components/ShippingZoneDetailsPage/utils.ts b/src/shipping/components/ShippingZoneDetailsPage/utils.ts index 320a3a362..0eb830f0c 100644 --- a/src/shipping/components/ShippingZoneDetailsPage/utils.ts +++ b/src/shipping/components/ShippingZoneDetailsPage/utils.ts @@ -1,11 +1,11 @@ import { ShippingZone_shippingZone } from "@saleor/shipping/types/ShippingZone"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import { FormData } from "./types"; +import { ShippingZoneUpdateFormData } from "./types"; export const getInitialFormData = ( shippingZone?: ShippingZone_shippingZone -): FormData => ({ +): ShippingZoneUpdateFormData => ({ description: shippingZone?.description || "", metadata: shippingZone?.metadata.map(mapMetadataItemToInput), name: shippingZone?.name || "", diff --git a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.stories.tsx b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.stories.tsx index 8ffc5459d..b012fee2b 100644 --- a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.stories.tsx +++ b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.stories.tsx @@ -41,6 +41,7 @@ const defaultChannels = [ ]; const props: ShippingZoneRatesCreatePageProps = { + formId: Symbol(), allChannelsCount: 3, channelErrors: [], disabled: false, diff --git a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx index 442972928..ffc823d89 100644 --- a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx +++ b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx @@ -4,12 +4,14 @@ import CardSpacer from "@saleor/components/CardSpacer"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; +import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; import { ShippingChannelsErrorFragment } from "@saleor/fragments/types/ShippingChannelsErrorFragment"; import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment"; import { ShippingMethodTypeFragment_postalCodeRules } from "@saleor/fragments/types/ShippingMethodTypeFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui"; import { validatePrice } from "@saleor/products/utils/validation"; @@ -29,7 +31,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import ShippingZonePostalCodes from "../ShippingZonePostalCodes"; import { ShippingZoneRateCommonFormData } from "../ShippingZoneRatesPage/types"; -export interface ShippingZoneRatesCreatePageProps { +export interface ShippingZoneRatesCreatePageProps extends WithFormId { allChannelsCount?: number; shippingChannels: ChannelShippingData[]; disabled: boolean; @@ -40,7 +42,7 @@ export interface ShippingZoneRatesCreatePageProps { saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; onDelete?: () => void; - onSubmit: (data: ShippingZoneRateCommonFormData) => void; + onSubmit: (data: ShippingZoneRateCommonFormData) => SubmitPromise; onPostalCodeInclusionChange: ( inclusion: PostalCodeRuleInclusionTypeEnum ) => void; @@ -68,7 +70,8 @@ export const ShippingZoneRatesCreatePage: React.FC { const intl = useIntl(); const isPriceVariant = variant === ShippingMethodTypeEnum.PRICE; @@ -85,7 +88,12 @@ export const ShippingZoneRatesCreatePage: React.FC + {({ change, data, hasChanged, submit, triggerChange, set }) => { const handleChannelsChange = createChannelsChangeHandler( shippingChannels, diff --git a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.stories.tsx b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.stories.tsx index 10993c8a1..345eec5d5 100644 --- a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.stories.tsx +++ b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.stories.tsx @@ -64,7 +64,8 @@ const props: ShippingZoneRatesPageProps = { toggle: () => undefined, toggleAll: () => undefined, toolbar: () => undefined, - variant: ShippingMethodTypeEnum.PRICE + variant: ShippingMethodTypeEnum.PRICE, + formId: Symbol() }; storiesOf("Views / Shipping / Shipping rate", module) diff --git a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx index 4fa1dfb18..ffb426648 100644 --- a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx +++ b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx @@ -4,6 +4,7 @@ import CardSpacer from "@saleor/components/CardSpacer"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; +import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider"; import Grid from "@saleor/components/Grid"; import Metadata from "@saleor/components/Metadata/Metadata"; import PageHeader from "@saleor/components/PageHeader"; @@ -11,6 +12,7 @@ import Savebar from "@saleor/components/Savebar"; import { ShippingChannelsErrorFragment } from "@saleor/fragments/types/ShippingChannelsErrorFragment"; import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment"; import { ShippingMethodTypeFragment_postalCodeRules } from "@saleor/fragments/types/ShippingMethodTypeFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui"; import { validatePrice } from "@saleor/products/utils/validation"; @@ -40,7 +42,8 @@ import { ShippingZoneRateUpdateFormData } from "./types"; export interface ShippingZoneRatesPageProps extends Pick>, - ListActions { + ListActions, + WithFormId { allChannelsCount?: number; shippingChannels: ChannelShippingData[]; disabled: boolean; @@ -53,7 +56,7 @@ export interface ShippingZoneRatesPageProps postalCodeRules: ShippingZone_shippingZone_shippingMethods_postalCodeRules[]; onBack: () => void; onDelete?: () => void; - onSubmit: (data: ShippingZoneRateUpdateFormData) => void; + onSubmit: (data: ShippingZoneRateUpdateFormData) => SubmitPromise; onPostalCodeInclusionChange: ( inclusion: PostalCodeRuleInclusionTypeEnum ) => void; @@ -90,6 +93,7 @@ export const ShippingZoneRatesPage: React.FC = ({ saveButtonBarState, postalCodeRules, variant, + formId, ...listProps }) => { const isPriceVariant = variant === ShippingMethodTypeEnum.PRICE; @@ -113,7 +117,12 @@ export const ShippingZoneRatesPage: React.FC = ({ } = useMetadataChangeTrigger(); return ( - + {({ change, data, hasChanged, submit, set, triggerChange }) => { const handleChannelsChange = createChannelsChangeHandler( shippingChannels, diff --git a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx index b65516d0b..c531e4910 100644 --- a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx +++ b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx @@ -4,7 +4,7 @@ import CommonDecorator from "@saleor/storybook/Decorator"; import { storiesOf } from "@storybook/react"; import React from "react"; -import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import { ShippingZoneUpdateFormData } from "../../components/ShippingZoneDetailsPage/types"; import ShippingZoneSettingsCard, { ShippingZoneSettingsCardProps } from "./ShippingZoneSettingsCard"; @@ -13,7 +13,7 @@ const props: ShippingZoneSettingsCardProps = { formData: { channels: ["channel1"], warehouses: ["warehouse1", "warehouse2"] - } as FormData, + } as ShippingZoneUpdateFormData, warehousesDisplayValues: [ { value: "warehouse1", diff --git a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx index bb6228ea4..0c69083e0 100644 --- a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx +++ b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx @@ -6,7 +6,7 @@ import { FormChange } from "@saleor/hooks/useForm"; import React from "react"; import { defineMessages, useIntl } from "react-intl"; -import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import { ShippingZoneUpdateFormData } from "../../components/ShippingZoneDetailsPage/types"; import ChannelsSection from "./ChannelsSection"; import WarehousesSection from "./WarehousesSection"; @@ -18,7 +18,7 @@ const messages = defineMessages({ }); export interface ShippingZoneSettingsCardProps { - formData: FormData; + formData: ShippingZoneUpdateFormData; warehousesDisplayValues: MultiAutocompleteChoiceType[]; warehousesChoices: MultiAutocompleteChoiceType[]; onWarehouseAdd: () => void; diff --git a/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx b/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx index 7fbe2ccee..f11797b2b 100644 --- a/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx +++ b/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx @@ -3,6 +3,7 @@ import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import RequirePermissions from "@saleor/components/RequirePermissions"; import { ShippingZoneFragment } from "@saleor/fragments/types/ShippingZoneFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; import { Backlink } from "@saleor/macaw-ui"; import { ListActions, PageListProps, UserPermissionProps } from "@saleor/types"; @@ -21,7 +22,7 @@ export interface ShippingZonesListPageProps shippingZones: ShippingZoneFragment[]; onBack: () => void; onRemove: (id: string) => void; - onSubmit: (unit: WeightUnitsEnum) => void; + onSubmit: (unit: WeightUnitsEnum) => SubmitPromise; } const ShippingZonesListPage: React.FC = ({ diff --git a/src/shipping/handlers.ts b/src/shipping/handlers.ts index a2a30a390..fa60e7d48 100644 --- a/src/shipping/handlers.ts +++ b/src/shipping/handlers.ts @@ -3,7 +3,7 @@ import { ShippingMethodTypeFragment_postalCodeRules } from "@saleor/fragments/ty import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; -import { getMutationErrors, getMutationState } from "@saleor/misc"; +import { extractMutationErrors, getMutationState } from "@saleor/misc"; import { CreateShippingRateVariables } from "@saleor/shipping/types/CreateShippingRate"; import { ShippingMethodChannelListingUpdateVariables } from "@saleor/shipping/types/ShippingMethodChannelListingUpdate"; import { UpdateShippingRateVariables } from "@saleor/shipping/types/UpdateShippingRate"; @@ -232,36 +232,43 @@ export function useShippingRateCreator( }); const createErrors = response.data.shippingPriceCreate.errors; - if (createErrors.length === 0) { - const rateId = response.data.shippingPriceCreate.shippingMethod.id; - const mutationResults = await Promise.all([ - updateShippingMethodChannelListing({ - variables: getShippingMethodChannelVariables( - rateId, - data.orderValueRestricted, - data.channelListings - ) - }) - ]); + if (createErrors.length > 0) { + return createErrors; + } - if ( - mutationResults.find( - result => getMutationErrors(result.data as any).length > 0 + const rateId = response.data.shippingPriceCreate.shippingMethod.id; + + const errors = await extractMutationErrors( + updateShippingMethodChannelListing({ + variables: getShippingMethodChannelVariables( + rateId, + data.orderValueRestricted, + data.channelListings ) - ) { - deleteShippingRate({ - variables: { - id: rateId - } - }); - } else { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - navigate(getUrl(shippingZoneId, rateId)); - } + }) + ); + + if (errors.length > 0) { + deleteShippingRate({ + variables: { + id: rateId + } + }); + + notify({ + status: "error", + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + + return errors; + } else { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(getUrl(shippingZoneId, rateId)); + return []; } }; diff --git a/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx b/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx index 6ab32717f..b90904909 100644 --- a/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx +++ b/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx @@ -29,6 +29,8 @@ import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandl import React from "react"; import { useIntl } from "react-intl"; +import { PRICE_RATES_CREATE_FORM_ID } from "./consts"; + export interface PriceRatesCreateProps { id: string; params: ShippingRateCreateUrlQueryParams; @@ -69,7 +71,12 @@ export const PriceRatesCreate: React.FC = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(allChannels, params?.action, { closeModal, openModal }); + } = useChannels( + allChannels, + params?.action, + { closeModal, openModal }, + { formId: PRICE_RATES_CREATE_FORM_ID } + ); const [state, dispatch] = React.useReducer(postalCodesReducer, { codesToDelete: [], @@ -146,6 +153,7 @@ export const PriceRatesCreate: React.FC = ({ )} = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(shippingChannels, params?.action, { closeModal, openModal }); + } = useChannels( + shippingChannels, + params?.action, + { closeModal, openModal }, + { formId: PRICE_RATES_UPDATE_FORM_ID } + ); const [updateShippingRate, updateShippingRateOpts] = useShippingRateUpdate( {} @@ -223,8 +230,11 @@ export const PriceRatesUpdate: React.FC = ({ state.codesToDelete ) }); + dispatch({ codesToDelete: [] }); + const errors = response.data.shippingPriceUpdate.errors; + if (errors.length === 0) { handleSuccess(); dispatch({ havePostalCodesChanged: false }); @@ -352,6 +362,7 @@ export const PriceRatesUpdate: React.FC = ({ onSubmit={handleProductAssign} /> = () => { } } }); + + const handleSubmit = (data: ShippingZoneCreateFormData) => + extractMutationErrors( + createShippingZone({ + variables: { + input: data + } + }) + ); + return ( navigate(shippingZonesListUrl())} - onSubmit={formData => - createShippingZone({ - variables: { - input: formData - } - }) - } + onSubmit={handleSubmit} saveButtonBarState={createShippingZoneOpts.status} /> ); diff --git a/src/shipping/views/ShippingZoneDetails/index.tsx b/src/shipping/views/ShippingZoneDetails/index.tsx index 62f255989..2956efacd 100644 --- a/src/shipping/views/ShippingZoneDetails/index.tsx +++ b/src/shipping/views/ShippingZoneDetails/index.tsx @@ -28,13 +28,21 @@ import { usePrivateMetadataUpdate } from "@saleor/utils/metadata/updateMetadata"; import { useWarehouseCreate } from "@saleor/warehouses/mutations"; +import { diff } from "fast-array-diff"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { findValueInEnum, getStringOrPlaceholder } from "../../../misc"; -import { CountryCode } from "../../../types/globalTypes"; +import { + extractMutationErrors, + findValueInEnum, + getStringOrPlaceholder +} from "../../../misc"; +import { + CountryCode, + ShippingZoneUpdateInput +} from "../../../types/globalTypes"; import ShippingZoneDetailsPage from "../../components/ShippingZoneDetailsPage"; -import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import { ShippingZoneUpdateFormData } from "../../components/ShippingZoneDetailsPage/types"; import { useShippingZone } from "../../queries"; import { shippingPriceRatesEditUrl, @@ -132,8 +140,10 @@ const ShippingZoneDetails: React.FC = ({ const [updateMetadata] = useMetadataUpdate({}); const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); - const updateData = async (submitData: FormData) => { - const warehouseDiff = arrayDiff( + const getParsedUpdateInput = ( + submitData: ShippingZoneUpdateFormData + ): ShippingZoneUpdateInput => { + const warehouseDiff = diff( data.shippingZone.warehouses.map(warehouse => warehouse.id), submitData.warehouses ); @@ -143,23 +153,26 @@ const ShippingZoneDetails: React.FC = ({ submitData.channels ); - const result = await updateShippingZone({ - variables: { - id, - input: { - addWarehouses: warehouseDiff.added, - addChannels: channelsDiff.added, - removeChannels: channelsDiff.removed, - description: submitData.description, - name: submitData.name, - removeWarehouses: warehouseDiff.removed - } - } - }); - - return result.data.shippingZoneUpdate.errors; + return { + addWarehouses: warehouseDiff.added, + addChannels: channelsDiff.added, + removeChannels: channelsDiff.removed, + description: submitData.description, + name: submitData.name, + removeWarehouses: warehouseDiff.removed + }; }; + const updateData = async (submitData: ShippingZoneUpdateFormData) => + extractMutationErrors( + updateShippingZone({ + variables: { + id, + input: getParsedUpdateInput(submitData) + } + }) + ); + const handleSubmit = createMetadataUpdateHandler( data?.shippingZone, updateData, diff --git a/src/shipping/views/ShippingZonesList.tsx b/src/shipping/views/ShippingZonesList.tsx index 94eb0d670..67f09ed17 100644 --- a/src/shipping/views/ShippingZonesList.tsx +++ b/src/shipping/views/ShippingZonesList.tsx @@ -13,7 +13,11 @@ import usePaginator, { import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; -import { getStringOrPlaceholder, maybe } from "@saleor/misc"; +import { + extractMutationErrors, + getStringOrPlaceholder, + maybe +} from "@saleor/misc"; import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; import { ListViews } from "@saleor/types"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; @@ -152,9 +156,11 @@ export const ShippingZonesList: React.FC = ({ } onRowClick={id => () => navigate(shippingZoneUrl(id))} onSubmit={unit => - updateDefaultWeightUnit({ - variables: { unit } - }) + extractMutationErrors( + updateDefaultWeightUnit({ + variables: { unit } + }) + ) } isChecked={isSelected} selected={listElements.length} diff --git a/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx b/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx index d938dd246..4a1cb8f8d 100644 --- a/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx +++ b/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx @@ -32,6 +32,8 @@ import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandl import React from "react"; import { useIntl } from "react-intl"; +import { WEIGHT_RATES_CREATE_FORM_ID } from "./consts"; + export interface WeightRatesCreateProps { id: string; params: ShippingRateCreateUrlQueryParams; @@ -75,7 +77,12 @@ export const WeightRatesCreate: React.FC = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(shippingChannels, params?.action, { closeModal, openModal }); + } = useChannels( + shippingChannels, + params?.action, + { closeModal, openModal }, + { formId: WEIGHT_RATES_CREATE_FORM_ID } + ); const [state, dispatch] = React.useReducer(postalCodesReducer, { codesToDelete: [], @@ -151,6 +158,7 @@ export const WeightRatesCreate: React.FC = ({ /> )} = ({ isChannelsModalOpen, setCurrentChannels, toggleAllChannels - } = useChannels(shippingChannels, params?.action, { closeModal, openModal }); + } = useChannels( + shippingChannels, + params?.action, + { closeModal, openModal }, + { formId: WEIGHT_RATES_UPDATE_FORM_ID } + ); const [updateShippingRate, updateShippingRateOpts] = useShippingRateUpdate( {} @@ -351,6 +358,7 @@ export const WeightRatesUpdate: React.FC = ({ onSubmit={handleProductAssign} /> = () => { : { companyName: data.companyName }; - const result = await updateShopSettings({ - variables: { - addressInput, - shopDomainInput: { - domain: data.domain, - name: data.name - }, - shopSettingsInput: { - description: data.description, - reserveStockDurationAnonymousUser: - data.reserveStockDurationAnonymousUser || null, - reserveStockDurationAuthenticatedUser: - data.reserveStockDurationAuthenticatedUser || null, - limitQuantityPerCheckout: - data.limitQuantityPerCheckout || null - }, - isCloudInstance: IS_CLOUD_INSTANCE - } - }); - return [ - ...result.data.shopAddressUpdate.errors, - ...(result.data.shopDomainUpdate?.errors || []), - ...result.data.shopSettingsUpdate.errors - ]; + return extractMutationErrors( + updateShopSettings({ + variables: { + addressInput, + shopDomainInput: { + domain: data.domain, + name: data.name + }, + shopSettingsInput: { + description: data.description, + reserveStockDurationAnonymousUser: + data.reserveStockDurationAnonymousUser || null, + reserveStockDurationAuthenticatedUser: + data.reserveStockDurationAuthenticatedUser || null + }, + isCloudInstance: IS_CLOUD_INSTANCE + } + }) + ); }; return ( diff --git a/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.tsx b/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.tsx index 9699aeb7c..c151717c8 100644 --- a/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.tsx +++ b/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.tsx @@ -10,6 +10,7 @@ import ConfirmButton from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import { StaffErrorFragment } from "@saleor/fragments/types/StaffErrorFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import { commonMessages } from "@saleor/intl"; import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui"; @@ -64,7 +65,7 @@ interface StaffAddMemberDialogProps extends SearchPageProps { fetchMorePermissionGroups: FetchMoreProps; open: boolean; onClose: () => void; - onConfirm: (data: AddMemberFormData) => void; + onConfirm: (data: AddMemberFormData) => SubmitPromise; } const StaffAddMemberDialog: React.FC = props => { diff --git a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx index 43e2ab708..8baba1c0d 100644 --- a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx +++ b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx @@ -100,7 +100,7 @@ const StaffDetailsPage: React.FC = ({ }; return ( - + {({ data: formData, change, hasChanged, submit, toggleValue }) => { const permissionGroupsChange = createMultiAutocompleteSelectHandler( toggleValue, diff --git a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx index 0c95756e6..40506ac56 100644 --- a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx +++ b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx @@ -10,6 +10,7 @@ import ConfirmButton from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import { buttonMessages } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; @@ -26,7 +27,7 @@ interface StaffPasswordResetDialogFormData { export interface StaffPasswordResetDialogProps extends DialogProps { confirmButtonState: ConfirmButtonTransitionState; errors: AccountErrorFragment[]; - onSubmit: (data: StaffPasswordResetDialogFormData) => void; + onSubmit: (data: StaffPasswordResetDialogFormData) => SubmitPromise; } const initialForm: StaffPasswordResetDialogFormData = { diff --git a/src/staff/mutations.ts b/src/staff/mutations.ts index e55cb92bb..2a38bf8cb 100644 --- a/src/staff/mutations.ts +++ b/src/staff/mutations.ts @@ -62,7 +62,7 @@ const staffMemberUpdateMutation = gql` } } `; -export const TypedStaffMemberUpdateMutation = TypedMutation< +export const useStaffMemberUpdateMutation = makeMutation< StaffMemberUpdate, StaffMemberUpdateVariables >(staffMemberUpdateMutation); diff --git a/src/staff/views/StaffDetails.tsx b/src/staff/views/StaffDetails.tsx index 293f47749..9f00a9719 100644 --- a/src/staff/views/StaffDetails.tsx +++ b/src/staff/views/StaffDetails.tsx @@ -7,7 +7,11 @@ import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages, errorMessages } from "@saleor/intl"; -import { getStringOrPlaceholder, maybe } from "@saleor/misc"; +import { + extractMutationErrors, + getStringOrPlaceholder, + maybe +} from "@saleor/misc"; import usePermissionGroupSearch from "@saleor/searches/usePermissionGroupSearch"; import { mapEdgesToItems } from "@saleor/utils/maps"; import React from "react"; @@ -21,8 +25,8 @@ import { TypedStaffAvatarDeleteMutation, TypedStaffAvatarUpdateMutation, TypedStaffMemberDeleteMutation, - TypedStaffMemberUpdateMutation, - useChangeStaffPassword + useChangeStaffPassword, + useStaffMemberUpdateMutation } from "../mutations"; import { TypedStaffMemberDetailsQuery } from "../queries"; import { ChangeStaffPassword } from "../types/ChangeStaffPassword"; @@ -79,6 +83,22 @@ export const StaffDetails: React.FC = ({ id, params }) => { variables: DEFAULT_INITIAL_SEARCH_DATA }); + const handleStaffMemberUpdateComplete = (data: StaffMemberUpdate) => { + if (!maybe(() => data.staffUpdate.errors.length !== 0)) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + } + }; + + const [ + updateStaffMember, + updateStaffMemberOpts + ] = useStaffMemberUpdateMutation({ + onCompleted: handleStaffMemberUpdateComplete + }); + return ( {({ data, loading }) => { @@ -88,14 +108,6 @@ export const StaffDetails: React.FC = ({ id, params }) => { return ; } - const handleStaffMemberUpdate = (data: StaffMemberUpdate) => { - if (!maybe(() => data.staffUpdate.errors.length !== 0)) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - } - }; const handleStaffMemberDelete = (data: StaffMemberDelete) => { if (!maybe(() => data.staffDelete.errors.length !== 0)) { notify({ @@ -129,178 +141,173 @@ export const StaffDetails: React.FC = ({ id, params }) => { } }; + const handleUpdate = (formData: StaffDetailsFormData) => + extractMutationErrors( + updateStaffMember({ + variables: { + id, + input: { + email: formData.email, + firstName: formData.firstName, + isActive: formData.isActive, + lastName: formData.lastName, + ...groupsDiff(data?.user, formData) + } + } + }) + ); + + // return ( + // + // {(updateStaffMember, updateResult) => { + return ( - - {(updateStaffMember, updateResult) => { - const handleSubmit = async (formData: StaffDetailsFormData) => { - const result = await updateStaffMember({ - variables: { - id, - input: { - email: formData.email, - firstName: formData.firstName, - isActive: formData.isActive, - lastName: formData.lastName, - ...groupsDiff(data?.user, formData) - } - } - }); + + {(deleteStaffMember, deleteResult) => ( + + {updateStaffAvatar => ( + + {(deleteStaffAvatar, deleteAvatarResult) => { + const isUserSameAsViewer = + user.user?.id === data?.user?.id; - return result.data.staffUpdate.errors; - }; - - return ( - - {(deleteStaffMember, deleteResult) => ( - - {updateStaffAvatar => ( - - {(deleteStaffAvatar, deleteAvatarResult) => { - const isUserSameAsViewer = - user.user?.id === data?.user?.id; - - return ( - <> - - - navigate( - staffMemberDetailsUrl(id, { - action: "change-password" - }) - ) - } - onDelete={() => - navigate( - staffMemberDetailsUrl(id, { - action: "remove" - }) - ) - } - onSubmit={handleSubmit} - onImageUpload={file => - updateStaffAvatar({ - variables: { - image: file - } - }) - } - onImageDelete={() => - navigate( - staffMemberDetailsUrl(id, { - action: "remove-avatar" - }) - ) - } - availablePermissionGroups={mapEdgesToItems( - searchPermissionGroupsOpts?.data?.search - )} - staffMember={staffMember} - saveButtonBarState={updateResult.status} - fetchMorePermissionGroups={{ - hasMore: - searchPermissionGroupsOpts.data?.search - .pageInfo.hasNextPage, - loading: searchPermissionGroupsOpts.loading, - onFetchMore: loadMorePermissionGroups - }} - onSearchChange={searchPermissionGroups} - /> - - - - - - - - - {getStringOrPlaceholder( - data?.user?.email - )} - - ) - }} - /> - - - - changePassword({ - variables: data - }) - } - /> - - ); - }} - - )} - - )} - - ); - }} - + return ( + <> + + + navigate( + staffMemberDetailsUrl(id, { + action: "change-password" + }) + ) + } + onDelete={() => + navigate( + staffMemberDetailsUrl(id, { + action: "remove" + }) + ) + } + onSubmit={handleUpdate} + onImageUpload={file => + updateStaffAvatar({ + variables: { + image: file + } + }) + } + onImageDelete={() => + navigate( + staffMemberDetailsUrl(id, { + action: "remove-avatar" + }) + ) + } + availablePermissionGroups={mapEdgesToItems( + searchPermissionGroupsOpts?.data?.search + )} + staffMember={staffMember} + saveButtonBarState={updateStaffMemberOpts.status} + fetchMorePermissionGroups={{ + hasMore: + searchPermissionGroupsOpts.data?.search.pageInfo + .hasNextPage, + loading: searchPermissionGroupsOpts.loading, + onFetchMore: loadMorePermissionGroups + }} + onSearchChange={searchPermissionGroups} + /> + + + + + + + + + {getStringOrPlaceholder( + data?.user?.email + )} + + ) + }} + /> + + + + changePassword({ + variables: data + }) + } + /> + + ); + }} + + )} + + )} + ); }} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index c440a0544..c87a2aba5 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -154109,14 +154109,32 @@ exports[`Storyshots Views / Pages / Page details loading 1`] = `
+ +
+
+ +
+ diff --git a/src/taxes/components/CountryListPage/CountryListPage.tsx b/src/taxes/components/CountryListPage/CountryListPage.tsx index 495a452f9..a4ee273a9 100644 --- a/src/taxes/components/CountryListPage/CountryListPage.tsx +++ b/src/taxes/components/CountryListPage/CountryListPage.tsx @@ -3,6 +3,7 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui"; @@ -14,7 +15,7 @@ import { CountryList_shop } from "../../types/CountryList"; import CountryList from "../CountryList"; import TaxConfiguration from "../TaxConfiguration"; -export interface FormData { +export interface TaxesConfigurationFormData { includeTax: boolean; showGross: boolean; chargeTaxesOnShipping: boolean; @@ -25,7 +26,7 @@ export interface CountryListPageProps { shop: CountryList_shop; onBack: () => void; onRowClick: (code: string) => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: TaxesConfigurationFormData) => SubmitPromise; onTaxFetch: () => void; } @@ -40,13 +41,13 @@ const CountryListPage: React.FC = ({ }) => { const intl = useIntl(); - const initialForm: FormData = { + const initialForm: TaxesConfigurationFormData = { chargeTaxesOnShipping: maybe(() => shop.chargeTaxesOnShipping, false), includeTax: maybe(() => shop.includeTaxesInPrices, false), showGross: maybe(() => shop.displayGrossPrices, false) }; return ( - + {({ change, data, hasChanged, submit }) => ( <> @@ -64,7 +65,7 @@ const CountryListPage: React.FC = ({ change(event, submit)} + onChange={event => change(event)} onTaxFetch={onTaxFetch} /> diff --git a/src/taxes/components/TaxConfiguration/TaxConfiguration.tsx b/src/taxes/components/TaxConfiguration/TaxConfiguration.tsx index f1c15cb24..aa3489405 100644 --- a/src/taxes/components/TaxConfiguration/TaxConfiguration.tsx +++ b/src/taxes/components/TaxConfiguration/TaxConfiguration.tsx @@ -7,10 +7,10 @@ import { Button, makeStyles } from "@saleor/macaw-ui"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { FormData } from "../CountryListPage"; +import { TaxesConfigurationFormData } from "../CountryListPage"; interface TaxConfigurationProps { - data: FormData; + data: TaxesConfigurationFormData; disabled: boolean; onChange: (event: React.ChangeEvent) => void; onTaxFetch: () => void; diff --git a/src/taxes/mutations.ts b/src/taxes/mutations.ts index 914b6b632..2833970cc 100644 --- a/src/taxes/mutations.ts +++ b/src/taxes/mutations.ts @@ -1,4 +1,5 @@ import { countryFragment, shopTaxesFragment } from "@saleor/fragments/taxes"; +import makeMutation from "@saleor/hooks/makeMutation"; import gql from "graphql-tag"; import { TypedMutation } from "../mutations"; @@ -22,7 +23,8 @@ const updateTaxSettings = gql` } } `; -export const TypedUpdateTaxSettings = TypedMutation< + +export const useTaxSettingsUpdateMutation = makeMutation< UpdateTaxSettings, UpdateTaxSettingsVariables >(updateTaxSettings); diff --git a/src/taxes/views/CountryList.tsx b/src/taxes/views/CountryList.tsx index 69b9986ea..461d2e7dd 100644 --- a/src/taxes/views/CountryList.tsx +++ b/src/taxes/views/CountryList.tsx @@ -5,9 +5,11 @@ import React from "react"; import { useIntl } from "react-intl"; import { configurationMenuUrl } from "../../configuration"; -import { maybe } from "../../misc"; -import CountryListPage from "../components/CountryListPage"; -import { TypedFetchTaxes, TypedUpdateTaxSettings } from "../mutations"; +import { extractMutationErrors, maybe } from "../../misc"; +import CountryListPage, { + TaxesConfigurationFormData +} from "../components/CountryListPage"; +import { TypedFetchTaxes, useTaxSettingsUpdateMutation } from "../mutations"; import { TypedCountryListQuery } from "../queries"; import { FetchTaxes } from "../types/FetchTaxes"; import { UpdateTaxSettings } from "../types/UpdateTaxSettings"; @@ -43,47 +45,50 @@ export const CountryList: React.FC = () => { } }; + const [ + updateTaxSettings, + updateTaxSettingsOpts + ] = useTaxSettingsUpdateMutation({ onCompleted: handleUpdateTaxSettings }); + + const handleSubmit = (data: TaxesConfigurationFormData) => + extractMutationErrors( + updateTaxSettings({ + variables: { + input: { + chargeTaxesOnShipping: data.chargeTaxesOnShipping, + displayGrossPrices: data.showGross, + includeTaxesInPrices: data.includeTax + } + } + }) + ); + return ( - - {(updateTaxSettings, updateTaxSettingsOpts) => ( - - {(fetchTaxes, fetchTaxesOpts) => ( - - {({ data, loading }) => ( - navigate(configurationMenuUrl)} - onRowClick={code => navigate(countryTaxRatesUrl(code))} - onSubmit={formData => - updateTaxSettings({ - variables: { - input: { - chargeTaxesOnShipping: formData.chargeTaxesOnShipping, - displayGrossPrices: formData.showGross, - includeTaxesInPrices: formData.includeTax - } - } - }) - } - onTaxFetch={fetchTaxes} - saveButtonBarState={updateTaxSettingsOpts.status} - shop={maybe(() => ({ - ...data.shop, - countries: data.shop.countries.filter( - country => country.vat - ) - }))} - /> - )} - + + {(fetchTaxes, fetchTaxesOpts) => ( + + {({ data, loading }) => ( + navigate(configurationMenuUrl)} + onRowClick={code => navigate(countryTaxRatesUrl(code))} + onSubmit={handleSubmit} + onTaxFetch={fetchTaxes} + saveButtonBarState={updateTaxSettingsOpts.status} + shop={maybe(() => ({ + ...data.shop, + countries: data.shop.countries.filter(country => country.vat) + }))} + /> )} - + )} - + ); }; + export default CountryList; diff --git a/src/translations/components/TranslationFields/TranslationFields.tsx b/src/translations/components/TranslationFields/TranslationFields.tsx index e55ac5d18..861de1037 100644 --- a/src/translations/components/TranslationFields/TranslationFields.tsx +++ b/src/translations/components/TranslationFields/TranslationFields.tsx @@ -6,6 +6,7 @@ import Grid from "@saleor/components/Grid"; import Hr from "@saleor/components/Hr"; import Skeleton from "@saleor/components/Skeleton"; import TablePagination from "@saleor/components/TablePagination"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { buttonMessages } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { Button, IconButton, makeStyles } from "@saleor/macaw-ui"; @@ -35,7 +36,10 @@ export interface TranslationFieldsProps { richTextResetKey: string; // temporary workaround TODO: fix rich text editor onEdit: (field: string) => void; onDiscard: () => void; - onSubmit: (field: TranslationField, data: string | OutputData) => void; + onSubmit: ( + field: TranslationField, + data: string | OutputData + ) => SubmitPromise; } const useStyles = makeStyles( diff --git a/src/translations/components/TranslationFields/TranslationFieldsLong.tsx b/src/translations/components/TranslationFields/TranslationFieldsLong.tsx index f112be9c9..59e929ecf 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsLong.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsLong.tsx @@ -1,5 +1,6 @@ import { TextField, Typography } from "@material-ui/core"; import Form from "@saleor/components/Form"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,7 +13,7 @@ interface TranslationFieldsLongProps { initial: string; saveButtonState: ConfirmButtonTransitionState; onDiscard: () => void; - onSubmit: (data: string) => void; + onSubmit: (data: string) => SubmitPromise; } const TranslationFieldsLong: React.FC = ({ @@ -27,6 +28,7 @@ const TranslationFieldsLong: React.FC = ({ return edit ? ( onSubmit(data.translation)} > diff --git a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx index 482724580..3e03c1b69 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx @@ -1,10 +1,12 @@ import { OutputData } from "@editorjs/editorjs"; import { Typography } from "@material-ui/core"; +import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditorContent from "@saleor/components/RichTextEditor/RichTextEditorContent"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import useRichText from "@saleor/utils/richText/useRichText"; -import React from "react"; +import React, { useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import TranslationFieldsSave from "./TranslationFieldsSave"; @@ -16,7 +18,7 @@ interface TranslationFieldsRichProps { saveButtonState: ConfirmButtonTransitionState; resetKey: string; onDiscard: () => void; - onSubmit: (data: OutputData) => void; + onSubmit: (data: OutputData) => SubmitPromise; } const TranslationFieldsRich: React.FC = ({ @@ -29,11 +31,16 @@ const TranslationFieldsRich: React.FC = ({ onSubmit }) => { const intl = useIntl(); + + const { setIsDirty, setExitDialogSubmitRef } = useExitFormDialog(); + const [content, change] = useRichText({ initial, - triggerChange: () => undefined + triggerChange: () => setIsDirty(true) }); + useEffect(() => setExitDialogSubmitRef(onSubmit), [content]); + const submit = () => onSubmit(content.current); return edit ? ( diff --git a/src/translations/components/TranslationFields/TranslationFieldsShort.tsx b/src/translations/components/TranslationFields/TranslationFieldsShort.tsx index b27125d2c..4b80f9212 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsShort.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsShort.tsx @@ -1,5 +1,6 @@ import { TextField, Typography } from "@material-ui/core"; import Form from "@saleor/components/Form"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,7 +13,7 @@ interface TranslationFieldsShortProps { initial: string; saveButtonState: ConfirmButtonTransitionState; onDiscard: () => void; - onSubmit: (data: string) => void; + onSubmit: (data: string) => SubmitPromise; } const TranslationFieldsShort: React.FC = ({ @@ -27,6 +28,7 @@ const TranslationFieldsShort: React.FC = ({ return edit ? ( onSubmit(data.translation)} > diff --git a/src/translations/types.ts b/src/translations/types.ts index 45be045a7..0a076a3fa 100644 --- a/src/translations/types.ts +++ b/src/translations/types.ts @@ -1,5 +1,6 @@ import { OutputData } from "@editorjs/editorjs"; import { ShopInfo_shop_languages } from "@saleor/components/Shop/types/ShopInfo"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; export enum TranslationInputFieldName { @@ -37,5 +38,8 @@ export interface TranslationsEntitiesPageProps { onEdit: (field: string) => void; onDiscard: () => void; onLanguageChange: (lang: string) => void; - onSubmit: (field: TranslationField, data: string | OutputData) => void; + onSubmit: ( + field: TranslationField, + data: string | OutputData + ) => SubmitPromise; } diff --git a/src/translations/views/TranslationsAttributes.tsx b/src/translations/views/TranslationsAttributes.tsx index 1e7340bce..1c5eac4f6 100644 --- a/src/translations/views/TranslationsAttributes.tsx +++ b/src/translations/views/TranslationsAttributes.tsx @@ -12,7 +12,7 @@ import { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; -import { getMutationState, maybe } from "../../misc"; +import { extractMutationErrors, getMutationState, maybe } from "../../misc"; import { LanguageCodeEnum } from "../../types/globalTypes"; import TranslationsAttributesPage, { fieldNames @@ -140,15 +140,18 @@ const TranslationsAttributes: React.FC = ({ [fieldNames.value, fieldNames.richTextValue].includes(fieldName) ) { const isRichText = fieldName === fieldNames.richTextValue; - updateAttributeValueTranslations({ - variables: { - id: fieldId, - input: isRichText - ? { richText: JSON.stringify(data) } - : { name: data as string }, - language: languageCode - } - }); + + return extractMutationErrors( + updateAttributeValueTranslations({ + variables: { + id: fieldId, + input: isRichText + ? { richText: JSON.stringify(data) } + : { name: data as string }, + language: languageCode + } + }) + ); } }; diff --git a/src/translations/views/TranslationsCategories.tsx b/src/translations/views/TranslationsCategories.tsx index cf1cf767e..6605211e8 100644 --- a/src/translations/views/TranslationsCategories.tsx +++ b/src/translations/views/TranslationsCategories.tsx @@ -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 { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; @@ -71,18 +72,19 @@ const TranslationsCategories: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string | OutputData - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); const translation = categoryTranslations?.data?.translation; diff --git a/src/translations/views/TranslationsCollections.tsx b/src/translations/views/TranslationsCollections.tsx index 65c96f944..0440f2b90 100644 --- a/src/translations/views/TranslationsCollections.tsx +++ b/src/translations/views/TranslationsCollections.tsx @@ -6,7 +6,7 @@ import { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../misc"; +import { extractMutationErrors, maybe } from "../../misc"; import { LanguageCodeEnum } from "../../types/globalTypes"; import TranslationsCollectionsPage from "../components/TranslationsCollectionsPage"; import { TypedUpdateCollectionTranslations } from "../mutations"; @@ -72,18 +72,19 @@ const TranslationsCollections: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); return ( = ({ name: fieldName }: TranslationField, data: string | any - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); const handleAttributeValueSubmit = ( { id }: TranslationField, data: OutputData - ) => { - updateAttributeValueTranslations({ - variables: { - id, - input: { richText: JSON.stringify(data) }, - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateAttributeValueTranslations({ + variables: { + id, + input: { richText: JSON.stringify(data) }, + language: languageCode + } + }) + ); const translation = pageTranslations?.data?.translation; diff --git a/src/translations/views/TranslationsProductVariants.tsx b/src/translations/views/TranslationsProductVariants.tsx index f1e09c6a1..8cd33f121 100644 --- a/src/translations/views/TranslationsProductVariants.tsx +++ b/src/translations/views/TranslationsProductVariants.tsx @@ -7,7 +7,7 @@ import { stringify as stringifyQs } from "qs"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../misc"; +import { extractMutationErrors, maybe } from "../../misc"; import { LanguageCodeEnum } from "../../types/globalTypes"; import TranslationsProductVariantsPage from "../components/TranslationsProductVariantsPage"; import { @@ -84,31 +84,33 @@ const TranslationsProductVariants: React.FC = const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); const handleAttributeValueSubmit = ( { id }: TranslationField, data: OutputData - ) => { - updateAttributeValueTranslations({ - variables: { - id, - input: { richText: JSON.stringify(data) }, - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateAttributeValueTranslations({ + variables: { + id, + input: { richText: JSON.stringify(data) }, + language: languageCode + } + }) + ); const translation = productVariantTranslations?.data?.translation; diff --git a/src/translations/views/TranslationsProducts.tsx b/src/translations/views/TranslationsProducts.tsx index e937b2aeb..b5e2dc589 100644 --- a/src/translations/views/TranslationsProducts.tsx +++ b/src/translations/views/TranslationsProducts.tsx @@ -7,7 +7,7 @@ import { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../misc"; +import { extractMutationErrors, maybe } from "../../misc"; import { LanguageCodeEnum } from "../../types/globalTypes"; import TranslationsProductsPage from "../components/TranslationsProductsPage"; import { @@ -82,31 +82,33 @@ const TranslationsProducts: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); const handleAttributeValueSubmit = ( { id }: TranslationField, data: OutputData - ) => { - updateAttributeValueTranslations({ - variables: { - id, - input: { richText: JSON.stringify(data) }, - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateAttributeValueTranslations({ + variables: { + id, + input: { richText: JSON.stringify(data) }, + language: languageCode + } + }) + ); const translation = productTranslations?.data?.translation; diff --git a/src/translations/views/TranslationsSales.tsx b/src/translations/views/TranslationsSales.tsx index 21f9db5f2..2c3a90d05 100644 --- a/src/translations/views/TranslationsSales.tsx +++ b/src/translations/views/TranslationsSales.tsx @@ -2,6 +2,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 { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; @@ -70,18 +71,20 @@ const TranslationsSales: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); + const translation = saleTranslations?.data?.translation; return ( diff --git a/src/translations/views/TranslationsShippingMethod.tsx b/src/translations/views/TranslationsShippingMethod.tsx index e20f5acb0..7a7296bbb 100644 --- a/src/translations/views/TranslationsShippingMethod.tsx +++ b/src/translations/views/TranslationsShippingMethod.tsx @@ -2,6 +2,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 { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; @@ -70,15 +71,17 @@ const TranslationsShippingMethod: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ fieldName, data }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ fieldName, data }), + language: languageCode + } + }) + ); + const translation = shippingMethodTranslations?.data?.translation; return ( diff --git a/src/translations/views/TranslationsVouchers.tsx b/src/translations/views/TranslationsVouchers.tsx index ac94da3b3..a0dd625c1 100644 --- a/src/translations/views/TranslationsVouchers.tsx +++ b/src/translations/views/TranslationsVouchers.tsx @@ -6,7 +6,7 @@ import { stringifyQs } from "@saleor/utils/urls"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../misc"; +import { extractMutationErrors, maybe } from "../../misc"; import { LanguageCodeEnum } from "../../types/globalTypes"; import TranslationsVouchersPage from "../components/TranslationsVouchersPage"; import { TypedUpdateVoucherTranslations } from "../mutations"; @@ -71,18 +71,19 @@ const TranslationsVouchers: React.FC = ({ const handleSubmit = ( { name: fieldName }: TranslationField, data: string - ) => { - updateTranslations({ - variables: { - id, - input: getParsedTranslationInputData({ - data, - fieldName - }), - language: languageCode - } - }); - }; + ) => + extractMutationErrors( + updateTranslations({ + variables: { + id, + input: getParsedTranslationInputData({ + data, + fieldName + }), + language: languageCode + } + }) + ); const translation = voucherTranslations?.data?.translation; diff --git a/src/utils/handlers/handleFormSubmit.ts b/src/utils/handlers/handleFormSubmit.ts deleted file mode 100644 index 2dc253801..000000000 --- a/src/utils/handlers/handleFormSubmit.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SubmitPromise } from "@saleor/hooks/useForm"; - -async function handleFormSubmit( - data: T, - onSubmit: (data: T) => SubmitPromise, - setChanged: (changed: boolean) => void -): Promise { - const errors = await onSubmit(data); - const ok = errors?.length === 0; - - if (ok) { - setChanged(false); - } - - return ok; -} - -export default handleFormSubmit; diff --git a/src/utils/handlers/metadataCreateHandler.ts b/src/utils/handlers/metadataCreateHandler.ts index d10abea4b..056045fc6 100644 --- a/src/utils/handlers/metadataCreateHandler.ts +++ b/src/utils/handlers/metadataCreateHandler.ts @@ -11,8 +11,13 @@ import { } from "../metadata/types/UpdatePrivateMetadata"; import { filterMetadataArray } from "./filterMetadataArray"; -function createMetadataCreateHandler( - create: (data: T) => Promise, +export interface CreateMetadataHandlerFunctionResult { + id?: string; + errors?: TError[]; +} + +function createMetadataCreateHandler( + create: (data: T) => Promise>, setMetadata: MutationFunction, setPrivateMetadata: MutationFunction< UpdatePrivateMetadata, @@ -20,10 +25,10 @@ function createMetadataCreateHandler( > ) { return async (data: T) => { - const id = await create(data); + const { id, errors } = await create(data); - if (id === null) { - return null; + if (id === null || !!errors?.length) { + return errors; } if (data.metadata.length > 0) { diff --git a/src/utils/handlers/metadataUpdateHandler.ts b/src/utils/handlers/metadataUpdateHandler.ts index 925d0b8be..e9335209f 100644 --- a/src/utils/handlers/metadataUpdateHandler.ts +++ b/src/utils/handlers/metadataUpdateHandler.ts @@ -1,5 +1,6 @@ import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MetadataErrorFragment } from "@saleor/fragments/types/MetadataErrorFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { MetadataInput } from "@saleor/types/globalTypes"; import { arrayDiff } from "@saleor/utils/arrays"; import { MutationFetchResult } from "react-apollo"; @@ -22,7 +23,7 @@ interface ObjectWithMetadata { function createMetadataUpdateHandler( initial: ObjectWithMetadata, - update: (data: TData) => Promise, + update: (data: TData) => SubmitPromise, updateMetadata: ( variables: UpdateMetadataVariables ) => Promise>, diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index d05ae08ca..b85583e85 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -1,5 +1,6 @@ import { OutputData } from "@editorjs/editorjs"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import isEqual from "lodash/isEqual"; import { MutableRefObject, useEffect, useRef, useState } from "react"; function useRichText(opts: { @@ -25,6 +26,10 @@ function useRichText(opts: { }, [opts.initial]); const change: RichTextEditorChange = newData => { + if (isEqual(data.current.blocks, newData.blocks)) { + return; + } + opts.triggerChange(); data.current = newData; }; diff --git a/src/warehouses/components/WarehouseCreatePage/WarehouseCreatePage.tsx b/src/warehouses/components/WarehouseCreatePage/WarehouseCreatePage.tsx index cb8d45963..9bacd3770 100644 --- a/src/warehouses/components/WarehouseCreatePage/WarehouseCreatePage.tsx +++ b/src/warehouses/components/WarehouseCreatePage/WarehouseCreatePage.tsx @@ -9,6 +9,7 @@ import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo" import { AddressTypeInput } from "@saleor/customers/types"; import { WarehouseErrorFragment } from "@saleor/fragments/types/WarehouseErrorFragment"; import useAddressValidation from "@saleor/hooks/useAddressValidation"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; @@ -29,7 +30,7 @@ export interface WarehouseCreatePageProps { errors: WarehouseErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; - onSubmit: (data: WarehouseCreatePageFormData) => void; + onSubmit: (data: WarehouseCreatePageFormData) => SubmitPromise; } const initialForm: WarehouseCreatePageFormData = { @@ -61,7 +62,7 @@ const WarehouseCreatePage: React.FC = ({ } = useAddressValidation(onSubmit); return ( - + {({ change, data, submit }) => { const countryChoices = mapCountriesToChoices(countries); const handleCountryChange = createSingleAutocompleteSelectHandler( diff --git a/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx b/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx index 2acc99cfc..52612ccd3 100644 --- a/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx +++ b/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx @@ -85,8 +85,8 @@ const WarehouseDetailsPage: React.FC = ({ }; return ( - - {({ change, data, hasChanged, set, submit }) => { + + {({ change, data, hasChanged, submit, set }) => { const countryChoices = mapCountriesToChoices(countries); const handleCountryChange = createSingleAutocompleteSelectHandler( change, diff --git a/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx b/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx index faa7430ff..249de7318 100644 --- a/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx +++ b/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx @@ -3,9 +3,15 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; -import { findValueInEnum, getMutationStatus } from "@saleor/misc"; +import { + extractMutationErrors, + findValueInEnum, + getMutationStatus +} from "@saleor/misc"; import { CountryCode } from "@saleor/types/globalTypes"; -import WarehouseCreatePage from "@saleor/warehouses/components/WarehouseCreatePage"; +import WarehouseCreatePage, { + WarehouseCreatePageFormData +} from "@saleor/warehouses/components/WarehouseCreatePage"; import { useWarehouseCreate } from "@saleor/warehouses/mutations"; import { warehouseListUrl, warehouseUrl } from "@saleor/warehouses/urls"; import React from "react"; @@ -29,6 +35,28 @@ const WarehouseCreate: React.FC = () => { }); const createWarehouseTransitionState = getMutationStatus(createWarehouseOpts); + const handleSubmit = (data: WarehouseCreatePageFormData) => + extractMutationErrors( + createWarehouse({ + variables: { + input: { + address: { + companyName: data.companyName, + city: data.city, + cityArea: data.cityArea, + country: findValueInEnum(data.country, CountryCode), + countryArea: data.countryArea, + phone: data.phone, + postalCode: data.postalCode, + streetAddress1: data.streetAddress1, + streetAddress2: data.streetAddress2 + }, + name: data.name + } + } + }) + ); + return ( <> { errors={createWarehouseOpts.data?.createWarehouse.errors || []} saveButtonBarState={createWarehouseTransitionState} onBack={() => navigate(warehouseListUrl())} - onSubmit={data => - createWarehouse({ - variables: { - input: { - address: { - companyName: data.companyName, - city: data.city, - cityArea: data.cityArea, - country: findValueInEnum(data.country, CountryCode), - countryArea: data.countryArea, - phone: data.phone, - postalCode: data.postalCode, - streetAddress1: data.streetAddress1, - streetAddress2: data.streetAddress2 - }, - name: data.name - } - } - }) - } + onSubmit={handleSubmit} /> ); diff --git a/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx b/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx index 7ca4590ac..2b2fdffff 100644 --- a/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx +++ b/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx @@ -5,6 +5,7 @@ import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; import { + extractMutationErrors, findValueInEnum, getMutationStatus, getStringOrPlaceholder @@ -78,31 +79,31 @@ const WarehouseDetails: React.FC = ({ id, params }) => { return navigate(warehouseListUrl())} />; } - const handleSubmit = async (data: WarehouseDetailsPageFormData) => { - const result = await updateWarehouse({ - variables: { - id, - input: { - address: { - companyName: data.companyName, - city: data.city, - cityArea: data.cityArea, - country: findValueInEnum(data.country, CountryCode), - countryArea: data.countryArea, - phone: data.phone, - postalCode: data.postalCode, - streetAddress1: data.streetAddress1, - streetAddress2: data.streetAddress2 - }, - name: data.name, - isPrivate: data.isPrivate, - clickAndCollectOption: data.clickAndCollectOption + const handleSubmit = async (data: WarehouseDetailsPageFormData) => + extractMutationErrors( + updateWarehouse({ + variables: { + id, + input: { + address: { + companyName: data.companyName, + city: data.city, + cityArea: data.cityArea, + country: findValueInEnum(data.country, CountryCode), + countryArea: data.countryArea, + phone: data.phone, + postalCode: data.postalCode, + streetAddress1: data.streetAddress1, + streetAddress2: data.streetAddress2 + }, + name: data.name, + isPrivate: data.isPrivate, + clickAndCollectOption: data.clickAndCollectOption + } } - } - }); + }) + ); - return result.data.updateWarehouse.errors; - }; return ( <> diff --git a/src/webhooks/components/WebhookInfo/WebhookInfo.tsx b/src/webhooks/components/WebhookInfo/WebhookInfo.tsx index 8c63a4982..dc11ac3b3 100644 --- a/src/webhooks/components/WebhookInfo/WebhookInfo.tsx +++ b/src/webhooks/components/WebhookInfo/WebhookInfo.tsx @@ -9,11 +9,11 @@ import getWebhookErrorMessage from "@saleor/utils/errors/webhooks"; import React from "react"; import { useIntl } from "react-intl"; -import { FormData } from "../WebhookDetailsPage"; +import { WebhookFormData } from "../WebhooksDetailsPage/WebhooksDetailsPage"; import { messages } from "./messages"; interface WebhookInfoProps { - data: FormData; + data: WebhookFormData; disabled: boolean; errors: WebhookErrorFragment[]; onChange: (event: React.ChangeEvent) => void; diff --git a/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx new file mode 100644 index 000000000..e72fb756c --- /dev/null +++ b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx @@ -0,0 +1,134 @@ +import Container from "@saleor/components/Container"; +import Form from "@saleor/components/Form"; +import FormSpacer from "@saleor/components/FormSpacer"; +import Grid from "@saleor/components/Grid"; +import PageHeader from "@saleor/components/PageHeader"; +import Savebar from "@saleor/components/Savebar"; +import { WebhookErrorFragment } from "@saleor/fragments/types/WebhookErrorFragment"; +import { SubmitPromise } from "@saleor/hooks/useForm"; +import { Backlink, ConfirmButtonTransitionState } from "@saleor/macaw-ui"; +import { + WebhookEventTypeAsyncEnum, + WebhookEventTypeSyncEnum +} from "@saleor/types/globalTypes"; +import WebhookEvents from "@saleor/webhooks/components/WebhookEvents"; +import WebhookInfo from "@saleor/webhooks/components/WebhookInfo"; +import WebhookStatus from "@saleor/webhooks/components/WebhookStatus"; +import { + createAsyncEventsSelectHandler, + createSyncEventsSelectHandler +} from "@saleor/webhooks/handlers"; +import { WebhookDetails_webhook } from "@saleor/webhooks/types/WebhookDetails"; +import { + mapAsyncEventsToChoices, + mapSyncEventsToChoices +} from "@saleor/webhooks/utils"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { getHeaderTitle } from "./messages"; + +export interface WebhookFormData { + syncEvents: WebhookEventTypeSyncEnum[]; + asyncEvents: WebhookEventTypeAsyncEnum[]; + isActive: boolean; + name: string; + secretKey: string | null; + targetUrl: string; +} + +export interface WebhookDetailsPageProps { + appName: string; + disabled: boolean; + errors: WebhookErrorFragment[]; + webhook?: WebhookDetails_webhook; + saveButtonBarState: ConfirmButtonTransitionState; + onBack: () => void; + onSubmit: (data: WebhookFormData) => SubmitPromise; +} + +const WebhookDetailsPage: React.FC = ({ + appName, + disabled, + errors, + webhook, + saveButtonBarState, + onBack, + onSubmit +}) => { + const intl = useIntl(); + + const initialForm: WebhookFormData = { + syncEvents: webhook?.syncEvents?.map(event => event.eventType) || [], + asyncEvents: webhook?.asyncEvents?.map(event => event.eventType) || [], + isActive: !!webhook?.isActive, + name: webhook?.name || "", + secretKey: webhook?.secretKey || "", + targetUrl: webhook?.targetUrl || "" + }; + + return ( + + {({ data, hasChanged, submit, change }) => { + const syncEventsChoices = disabled + ? [] + : mapSyncEventsToChoices(Object.values(WebhookEventTypeSyncEnum)); + const asyncEventsChoices = disabled + ? [] + : mapAsyncEventsToChoices( + Object.values(WebhookEventTypeAsyncEnum), + data.asyncEvents + ); + + const handleSyncEventsSelect = createSyncEventsSelectHandler( + change, + data.syncEvents + ); + const handleAsyncEventsSelect = createAsyncEventsSelectHandler( + change, + data.asyncEvents + ); + + return ( + + {appName} + + +
+ +
+
+ + + +
+
+ +
+ ); + }} + + ); +}; +WebhookDetailsPage.displayName = "WebhookDetailsPage"; +export default WebhookDetailsPage; diff --git a/src/webhooks/components/WebhooksDetailsPage/messages.ts b/src/webhooks/components/WebhooksDetailsPage/messages.ts new file mode 100644 index 000000000..c2aa0b725 --- /dev/null +++ b/src/webhooks/components/WebhooksDetailsPage/messages.ts @@ -0,0 +1,35 @@ +import { getStringOrPlaceholder } from "@saleor/misc"; +import { WebhookDetails_webhook } from "@saleor/webhooks/types/WebhookDetails"; +import { isUnnamed } from "@saleor/webhooks/utils"; +import { IntlShape } from "react-intl"; +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + header: { + defaultMessage: "Unnamed Webhook Details", + description: "header" + }, + headerNamed: { + defaultMessage: "{webhookName} Details", + description: "header" + }, + headerCreate: { + defaultMessage: "Create Webhook", + description: "header" + } +}); + +export const getHeaderTitle = ( + intl: IntlShape, + webhook?: WebhookDetails_webhook +) => { + if (!webhook) { + return intl.formatMessage(messages.headerCreate); + } + if (isUnnamed(webhook)) { + return intl.formatMessage(messages.header); + } + return intl.formatMessage(messages.headerNamed, { + webhookName: getStringOrPlaceholder(webhook?.name) + }); +}; diff --git a/src/webhooks/views/WebhooksCreate.tsx b/src/webhooks/views/WebhooksCreate.tsx index 887858a2f..5b08b711a 100644 --- a/src/webhooks/views/WebhooksCreate.tsx +++ b/src/webhooks/views/WebhooksCreate.tsx @@ -4,6 +4,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; +import { extractMutationErrors } from "@saleor/misc"; import { WebhookEventTypeAsyncEnum } from "@saleor/types/globalTypes"; import React from "react"; import { useIntl } from "react-intl"; @@ -40,23 +41,25 @@ export const WebhooksCreate: React.FC = ({ id }) => { const handleBack = () => navigate(customAppUrl(id)); const handleSubmit = (data: FormData) => - webhookCreate({ - variables: { - input: { - app: id, - syncEvents: data.syncEvents, - asyncEvents: data.asyncEvents.includes( - WebhookEventTypeAsyncEnum.ANY_EVENTS - ) - ? [WebhookEventTypeAsyncEnum.ANY_EVENTS] - : data.asyncEvents, - isActive: data.isActive, - name: data.name, - secretKey: data.secretKey, - targetUrl: data.targetUrl + extractMutationErrors( + webhookCreate({ + variables: { + input: { + app: id, + syncEvents: data.syncEvents, + asyncEvents: data.asyncEvents.includes( + WebhookEventTypeAsyncEnum.ANY_EVENTS + ) + ? [WebhookEventTypeAsyncEnum.ANY_EVENTS] + : data.asyncEvents, + isActive: data.isActive, + name: data.name, + secretKey: data.secretKey, + targetUrl: data.targetUrl + } } - } - }); + }) + ); return ( <> diff --git a/src/webhooks/views/WebhooksDetails.tsx b/src/webhooks/views/WebhooksDetails.tsx index 56fe01977..0b601ca52 100644 --- a/src/webhooks/views/WebhooksDetails.tsx +++ b/src/webhooks/views/WebhooksDetails.tsx @@ -9,8 +9,9 @@ import { WebhookUpdate } from "@saleor/webhooks/types/WebhookUpdate"; import React from "react"; import { useIntl } from "react-intl"; -import { getStringOrPlaceholder } from "../../misc"; +import { extractMutationErrors, getStringOrPlaceholder } from "../../misc"; import WebhookDetailsPage from "../components/WebhookDetailsPage"; +import { WebhookFormData } from "../components/WebhooksDetailsPage/WebhooksDetailsPage"; import { useWebhookUpdateMutation } from "../mutations"; import { useWebhooksDetailsQuery } from "../queries"; @@ -52,6 +53,27 @@ export const WebhooksDetails: React.FC = ({ id }) => { return ; } + const handleSubmit = (data: WebhookFormData) => + extractMutationErrors( + webhookUpdate({ + variables: { + id, + input: { + syncEvents: data.syncEvents, + asyncEvents: data.asyncEvents.includes( + WebhookEventTypeAsyncEnum.ANY_EVENTS + ) + ? [WebhookEventTypeAsyncEnum.ANY_EVENTS] + : data.asyncEvents, + isActive: data.isActive, + name: data.name, + secretKey: data.secretKey, + targetUrl: data.targetUrl + } + } + }) + ); + return ( <> = ({ id }) => { saveButtonBarState={webhookUpdateOpts.status} webhook={webhook} onBack={handleOnBack} - onSubmit={data => { - webhookUpdate({ - variables: { - id, - input: { - syncEvents: data.syncEvents, - asyncEvents: data.asyncEvents.includes( - WebhookEventTypeAsyncEnum.ANY_EVENTS - ) - ? [WebhookEventTypeAsyncEnum.ANY_EVENTS] - : data.asyncEvents, - isActive: data.isActive, - name: data.name, - secretKey: data.secretKey, - targetUrl: data.targetUrl - } - } - }); - }} + onSubmit={handleSubmit} /> );