diff --git a/CHANGELOG.md b/CHANGELOG.md index d4538a56f..213aa9003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable, unreleased changes to this project will be documented in this file. - Handle form errors before product creation - #2299 by @orzechdev - Fix no product error on unconfirmed order lines - #2324 by @orzechdev - Enable save button on discount pages - #2319 by @orzechdev +- Enable save button on page pages - #2325 by @orzechdev ## 3.4 diff --git a/cypress/elements/pages/pages-list.js b/cypress/elements/pages/pages-list.js index 2fcfd7cd9..896194460 100644 --- a/cypress/elements/pages/pages-list.js +++ b/cypress/elements/pages/pages-list.js @@ -1,3 +1,4 @@ export const PAGES_LIST = { - createPageButton: '[data-test-id="create-page"]' + createPageButton: '[data-test-id="create-page"]', + dialogPageTypeInput: "[data-test-id='dialog-page-type']", }; diff --git a/cypress/support/pages/pagesPage.js b/cypress/support/pages/pagesPage.js index 7f01aab52..1152948fa 100644 --- a/cypress/support/pages/pagesPage.js +++ b/cypress/support/pages/pagesPage.js @@ -11,6 +11,17 @@ export const attributesTypes = { BOOLEAN: addBooleanAttributeValue, NUMERIC: addNumericAttributeValue, }; + +export function fillUpPageTypeDialog({ pageTypeName }) { + const organization = {}; + return cy + .fillAutocompleteSelect(PAGES_LIST.dialogPageTypeInput, pageTypeName) + .then(selected => { + organization.pageType = selected; + return organization; + }); +} + export function createPage({ pageName, pageTypeName, @@ -52,6 +63,9 @@ function openCreatePageAndFillUpGeneralFields({ }) { cy.visit(urlList.pages) .get(PAGES_LIST.createPageButton) + .click(); + fillUpPageTypeDialog({ pageTypeName }); + cy.get(BUTTON_SELECTORS.submit) .click() .get(PAGE_DETAILS.nameInput) .type(pageName); diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index b415b5b5b..c2a3907b6 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2059,6 +2059,10 @@ "context": "invoice create date prefix", "string": "created" }, + "F0N1SC": { + "context": "dialog header", + "string": "Select a page type" + }, "F3Upht": { "string": "Product type deleted" }, @@ -2596,6 +2600,10 @@ "context": "section header", "string": "You are about to install {name}" }, + "Id9vlh": { + "context": "page seo options description", + "string": "Add search engine title and description to make this page easier to find" + }, "IeoGgH": { "context": "variant created success message", "string": "Variant created" @@ -4211,6 +4219,10 @@ "context": "dialog content", "string": "Removed sale" }, + "V45+rx": { + "context": "input label", + "string": "Page type" + }, "V8FhTt": { "context": "collection label", "string": "Hidden" @@ -5890,9 +5902,6 @@ "context": "product availability", "string": "Hide in product listings" }, - "jZbT0O": { - "string": "Add search engine title and description to make this page easier to find" - }, "jd/LWa": { "string": "Voucher applies to all countries" }, diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index adf987adb..aebd8ee8c 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -35,6 +35,7 @@ import { useIntl } from "react-intl"; import PageInfo from "../PageInfo"; import PageOrganizeContent from "../PageOrganizeContent"; import PageForm, { PageData, PageUpdateHandlers } from "./form"; +import { messages } from "./messages"; export interface PageDetailsPageProps { loading: boolean; @@ -68,7 +69,7 @@ export interface PageDetailsPageProps { const PageDetailsPage: React.FC = ({ loading, - errors, + errors: apiErrors, page, pageTypes: pageTypeChoiceList, referencePages, @@ -142,148 +143,136 @@ const PageDetailsPage: React.FC = ({ {({ change, data, + validationErrors, handlers, submit, - isSaveDisabled, attributeRichTextGetters, - }) => ( - - - {intl.formatMessage(sectionNames.pages)} - - - -
- - - - - {data.attributes.length > 0 && ( - - )} - - -
-
- - - -
-
- navigate(pageListUrl())} - onDelete={page === null ? undefined : onRemove} - onSubmit={submit} - /> - {canOpenAssignReferencesAttributeDialog && ( - - handleAssignReferenceAttribute(attributeValues, data, handlers) + }) => { + const errors = [...apiErrors, ...validationErrors]; + + return ( + + + {intl.formatMessage(sectionNames.pages)} + + - )} - - )} + +
+ + + + + {data.attributes.length > 0 && ( + + )} + + +
+
+ + + +
+
+ navigate(pageListUrl())} + onDelete={page === null ? undefined : onRemove} + onSubmit={submit} + /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute( + attributeValues, + data, + handlers, + ) + } + /> + )} +
+ ); + }} ); }; diff --git a/src/pages/components/PageDetailsPage/form.tsx b/src/pages/components/PageDetailsPage/form.tsx index c2700d6a0..b3e18abfc 100644 --- a/src/pages/components/PageDetailsPage/form.tsx +++ b/src/pages/components/PageDetailsPage/form.tsx @@ -20,6 +20,7 @@ import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; import { MetadataFormData } from "@saleor/components/Metadata"; import { PageDetailsFragment, + PageErrorWithAttributesFragment, SearchPagesQuery, SearchPageTypesQuery, SearchProductsQuery, @@ -39,6 +40,7 @@ import { getAttributeInputFromPageType, } from "@saleor/pages/utils/data"; import { createPageTypeSelectHandler } from "@saleor/pages/utils/handlers"; +import { validatePageCreateData } from "@saleor/pages/utils/validation"; import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types"; import getPublicationData from "@saleor/utils/data/getPublicationData"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; @@ -47,7 +49,7 @@ import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTr import { RichTextContext } from "@saleor/utils/richText/context"; import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText"; import useRichText from "@saleor/utils/richText/useRichText"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; export interface PageFormData extends MetadataFormData { isPublished: boolean; @@ -85,6 +87,7 @@ export interface UsePageUpdateFormOutput extends CommonUseFormResultWithHandlers, RichTextProps { valid: boolean; + validationErrors: PageErrorWithAttributesFragment[]; } export type UsePageUpdateFormRenderProps = Omit< @@ -142,6 +145,9 @@ function usePageForm( confirmLeave: true, }, ); + const [validationErrors, setValidationErrors] = useState< + PageErrorWithAttributesFragment[] + >([]); const attributes = useFormset( pageExists @@ -160,7 +166,11 @@ function usePageForm( }); const attributesWithNewFileValue = useFormset([]); - const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({ + const { + setExitDialogSubmitRef, + setIsSubmitDisabled, + setIsDirty, + } = useExitFormDialog({ formId, }); @@ -247,7 +257,15 @@ function usePageForm( }); const handleSubmit = async (data: PageData) => { - const errors = await onSubmit(data); + let errors = validatePageCreateData(data); + + setValidationErrors(errors); + + if (errors.length) { + return errors; + } + + errors = await onSubmit(data); if (!errors?.length && pageExists) { attributesWithNewFileValue.set([]); @@ -261,18 +279,34 @@ function usePageForm( onSubmit: handleSubmit, }); - const submit = async () => handleFormSubmit(await getSubmitData()); + const submit = async () => { + const errors = await handleFormSubmit(await getSubmitData()); + + if (errors.length) { + setIsSubmitDisabled(isSaveDisabled); + setIsDirty(true); + } + + return errors; + }; useEffect(() => setExitDialogSubmitRef(submit), [submit]); const valid = pageExists || !!opts.selectedPageType; const isSaveDisabled = disabled || !valid; - setIsSubmitDisabled(isSaveDisabled); + + useEffect(() => { + setIsSubmitDisabled(isSaveDisabled); + if (!pageExists) { + setIsDirty(true); + } + }, [isSaveDisabled]); return { change: handleChange, data, + validationErrors, valid, handlers: { changeMetadata, diff --git a/src/pages/components/PageDetailsPage/messages.ts b/src/pages/components/PageDetailsPage/messages.ts new file mode 100644 index 000000000..c18949a71 --- /dev/null +++ b/src/pages/components/PageDetailsPage/messages.ts @@ -0,0 +1,30 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + title: { + id: "gr53VQ", + defaultMessage: "Create Page", + description: "page header", + }, + seoOptionsDescription: { + id: "Id9vlh", + defaultMessage: + "Add search engine title and description to make this page easier to find", + description: "page seo options description", + }, + hiddenLabel: { + id: "/TK7QD", + defaultMessage: "Hidden", + description: "page label", + }, + hiddenSecondLabel: { + id: "GZgjK7", + defaultMessage: "will be visible from {date}", + description: "page", + }, + visibleLabel: { + id: "X26jCC", + defaultMessage: "Visible", + description: "page label", + }, +}); diff --git a/src/pages/components/PageListPage/PageListPage.tsx b/src/pages/components/PageListPage/PageListPage.tsx index f939b220f..6d5b6896d 100644 --- a/src/pages/components/PageListPage/PageListPage.tsx +++ b/src/pages/components/PageListPage/PageListPage.tsx @@ -5,7 +5,6 @@ import PageHeader from "@saleor/components/PageHeader"; import { PageFragment } from "@saleor/graphql"; import { sectionNames } from "@saleor/intl"; import { - pageCreateUrl, PageListUrlDialog, PageListUrlQueryParams, PageListUrlSortField, @@ -28,11 +27,13 @@ export interface PageListPageProps pages: PageFragment[]; params: PageListUrlQueryParams; actionDialogOpts: PageListActionDialogOpts; + onAdd: () => void; } const PageListPage: React.FC = ({ params, actionDialogOpts, + onAdd, ...listProps }) => { const intl = useIntl(); @@ -40,11 +41,7 @@ const PageListPage: React.FC = ({ return ( - @@ -158741,20 +158739,18 @@ exports[`Storyshots Views / Pages / Page list loading 1`] = ` @@ -159136,20 +159132,18 @@ exports[`Storyshots Views / Pages / Page list no data 1`] = ` @@ -159402,6 +159396,12 @@ exports[`Storyshots Views / Pages / Page list no data 1`] = ` `; +exports[`Storyshots Views / Pages / Page type dialog default 1`] = ` +
+`; + exports[`Storyshots Views / Permission Groups / Permission Group Create default 1`] = `