diff --git a/schema.graphql b/schema.graphql index 1a05aa1ed..f738d9deb 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1184,6 +1184,7 @@ enum CollectionSortField { NAME AVAILABILITY PRODUCT_COUNT + PUBLICATION_DATE } input CollectionSortingInput { @@ -2306,6 +2307,7 @@ type Margin { type Menu implements Node { id: ID! name: String! + slug: String! items: [MenuItem] } @@ -2334,6 +2336,7 @@ type MenuCreate { input MenuCreateInput { name: String! + slug: String items: [MenuItemInput] } @@ -2363,10 +2366,12 @@ enum MenuErrorCode { input MenuFilterInput { search: String + slug: [String] } input MenuInput { name: String + slug: String } type MenuItem implements Node { @@ -3229,7 +3234,7 @@ type OrderVoid { orderErrors: [OrderError!]! } -type Page implements Node { +type Page implements Node & ObjectWithMetadata { seoTitle: String seoDescription: String id: ID! @@ -3240,6 +3245,10 @@ type Page implements Node { isPublished: Boolean! slug: String! created: DateTime! + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! + privateMeta: [MetaStore]! @deprecated(reason: "Use the `privetaMetadata` field. This field will be removed after 2020-07-31.") + meta: [MetaStore]! @deprecated(reason: "Use the `metadata` field. This field will be removed after 2020-07-31.") translation(languageCode: LanguageCodeEnum!): PageTranslation } @@ -3792,6 +3801,7 @@ input ProductFilterInput { price: PriceRangeInput minimalPrice: PriceRangeInput productTypes: [ID] + ids: [ID] } type ProductImage implements Node { @@ -3878,6 +3888,7 @@ enum ProductOrderField { DATE TYPE PUBLISHED + PUBLICATION_DATE } type ProductPricingInfo { @@ -4288,7 +4299,7 @@ type Query { draftOrders(sortBy: OrderSortingInput, filter: OrderDraftFilterInput, created: ReportingPeriod, before: String, after: String, first: Int, last: Int): OrderCountableConnection ordersTotal(period: ReportingPeriod): TaxedMoney orderByToken(token: UUID!): Order - menu(id: ID, name: String): Menu + menu(id: ID, name: String, slug: String): Menu menus(sortBy: MenuSortingInput, filter: MenuFilterInput, before: String, after: String, first: Int, last: Int): MenuCountableConnection menuItem(id: ID!): MenuItem menuItems(sortBy: MenuItemSortingInput, filter: MenuItemFilterInput, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection @@ -4755,10 +4766,10 @@ type Shop { defaultMailSenderAddress: String description: String domain: Domain! - homepageCollection: Collection + homepageCollection: Collection @deprecated(reason: "Use the `collection` query with the `slug` parameter. This field will be removed in Saleor 3.0") languages: [LanguageDisplay]! name: String! - navigation: Navigation + navigation: Navigation @deprecated(reason: "Fetch menus using the `menu` query with `slug` parameter.") permissions: [Permission]! phonePrefixes: [String]! headerText: String diff --git a/src/fragments/pages.ts b/src/fragments/pages.ts index ea2e13d60..ad6cb6b4a 100644 --- a/src/fragments/pages.ts +++ b/src/fragments/pages.ts @@ -1,5 +1,7 @@ import gql from "graphql-tag"; +import { metadataFragment } from "./metadata"; + export const pageFragment = gql` fragment PageFragment on Page { id @@ -11,8 +13,10 @@ export const pageFragment = gql` export const pageDetailsFragment = gql` ${pageFragment} + ${metadataFragment} fragment PageDetailsFragment on Page { ...PageFragment + ...MetadataFragment contentJson seoTitle seoDescription diff --git a/src/fragments/types/MetadataFragment.ts b/src/fragments/types/MetadataFragment.ts index 4c1b70245..9404adc18 100644 --- a/src/fragments/types/MetadataFragment.ts +++ b/src/fragments/types/MetadataFragment.ts @@ -19,7 +19,7 @@ export interface MetadataFragment_privateMetadata { } export interface MetadataFragment { - __typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice"; + __typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "Page" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice"; metadata: (MetadataFragment_metadata | null)[]; privateMetadata: (MetadataFragment_privateMetadata | null)[]; } diff --git a/src/fragments/types/PageDetailsFragment.ts b/src/fragments/types/PageDetailsFragment.ts index 1b1e48c92..a6f606772 100644 --- a/src/fragments/types/PageDetailsFragment.ts +++ b/src/fragments/types/PageDetailsFragment.ts @@ -6,12 +6,26 @@ // GraphQL fragment: PageDetailsFragment // ==================================================== +export interface PageDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface PageDetailsFragment { __typename: "Page"; id: string; title: string; slug: string; isPublished: boolean; + metadata: (PageDetailsFragment_metadata | null)[]; + privateMetadata: (PageDetailsFragment_privateMetadata | null)[]; contentJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 3253018d5..8350b9b99 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -4,6 +4,7 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; 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 PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -11,6 +12,8 @@ import VisibilityCard from "@saleor/components/VisibilityCard"; import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; import { sectionNames } from "@saleor/intl"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import { ContentState, convertFromRaw, @@ -24,7 +27,7 @@ import { maybe } from "../../../misc"; import { PageDetails_page } from "../../types/PageDetails"; import PageInfo from "../PageInfo"; -export interface FormData { +export interface FormData extends MetadataFormData { content: RawDraftContentState; isPublished: boolean; publicationDate: string; @@ -56,6 +59,12 @@ const PageDetailsPage: React.FC = ({ }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const pageExists = page !== null; const initialForm: FormData = { @@ -63,109 +72,126 @@ const PageDetailsPage: React.FC = ({ () => JSON.parse(page.contentJson), convertToRaw(ContentState.createFromText("")) ), - isPublished: maybe(() => page.isPublished, false), - publicationDate: maybe(() => page.publicationDate, ""), - seoDescription: maybe(() => page.seoDescription || "", ""), - seoTitle: maybe(() => page.seoTitle || "", ""), - slug: maybe(() => page.slug, ""), - title: maybe(() => page.title, "") + isPublished: page?.isPublished, + metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], + privateMetadata: pageExists + ? page?.privateMetadata?.map(mapMetadataItemToInput) + : [], + publicationDate: page?.publicationDate || "", + seoDescription: page?.seoDescription || "", + seoTitle: page?.seoTitle || "", + slug: page?.slug || "", + title: page?.title || "" }; - const handleSubmit = (data: FormData) => onSubmit(getParsedData(data)); + const handleSubmit = (data: FormData) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; - const getParsedData = (data: FormData) => ({ - ...data, - isPublished: data.isPublished || !!data.publicationDate - }); + onSubmit({ + ...data, + isPublished: data.isPublished || !!data.publicationDate, + metadata, + privateMetadata + }); + }; return (
- {({ change, data, hasChanged, submit }) => ( - - - {intl.formatMessage(sectionNames.pages)} - - page.title) - } - /> - -
- - - - convertFromRaw(data.content) - .getPlainText() - .slice(0, 300), - "" - )} - onChange={change} - slug={data.slug} - slugPlaceholder={data.title} - title={data.seoTitle} - titlePlaceholder={data.title} - helperText={intl.formatMessage({ - defaultMessage: - "Add search engine title and description to make this page easier to find" - })} - /> -
-
- - -
-
- -
- )} + {({ change, data, hasChanged, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.pages)} + + page.title) + } + /> + +
+ + + + convertFromRaw(data.content) + .getPlainText() + .slice(0, 300), + "" + )} + onChange={change} + slug={data.slug} + slugPlaceholder={data.title} + title={data.seoTitle} + titlePlaceholder={data.title} + helperText={intl.formatMessage({ + defaultMessage: + "Add search engine title and description to make this page easier to find" + })} + /> + + +
+
+ + +
+
+ +
+ ); + }}
); }; diff --git a/src/pages/fixtures.ts b/src/pages/fixtures.ts index 86739b16c..6aba5a7ad 100644 --- a/src/pages/fixtures.ts +++ b/src/pages/fixtures.ts @@ -37,6 +37,14 @@ export const page: PageDetails_page = { contentJson: JSON.stringify(content), id: "Kzx152sEm==", isPublished: false, + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], + privateMetadata: [], publicationDate: "", seoDescription: "About", seoTitle: "About", diff --git a/src/pages/types/PageCreate.ts b/src/pages/types/PageCreate.ts index 2101f6a48..97b77e7aa 100644 --- a/src/pages/types/PageCreate.ts +++ b/src/pages/types/PageCreate.ts @@ -14,12 +14,26 @@ export interface PageCreate_pageCreate_errors { field: string | null; } +export interface PageCreate_pageCreate_page_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageCreate_pageCreate_page_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface PageCreate_pageCreate_page { __typename: "Page"; id: string; title: string; slug: string; isPublished: boolean; + metadata: (PageCreate_pageCreate_page_metadata | null)[]; + privateMetadata: (PageCreate_pageCreate_page_privateMetadata | null)[]; contentJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/pages/types/PageDetails.ts b/src/pages/types/PageDetails.ts index ffc94dd3a..d77643d92 100644 --- a/src/pages/types/PageDetails.ts +++ b/src/pages/types/PageDetails.ts @@ -6,12 +6,26 @@ // GraphQL query operation: PageDetails // ==================================================== +export interface PageDetails_page_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageDetails_page_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface PageDetails_page { __typename: "Page"; id: string; title: string; slug: string; isPublished: boolean; + metadata: (PageDetails_page_metadata | null)[]; + privateMetadata: (PageDetails_page_privateMetadata | null)[]; contentJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/pages/types/PageUpdate.ts b/src/pages/types/PageUpdate.ts index ae022c75a..3060b0b72 100644 --- a/src/pages/types/PageUpdate.ts +++ b/src/pages/types/PageUpdate.ts @@ -14,12 +14,26 @@ export interface PageUpdate_pageUpdate_errors { field: string | null; } +export interface PageUpdate_pageUpdate_page_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageUpdate_pageUpdate_page_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface PageUpdate_pageUpdate_page { __typename: "Page"; id: string; title: string; slug: string; isPublished: boolean; + metadata: (PageUpdate_pageUpdate_page_metadata | null)[]; + privateMetadata: (PageUpdate_pageUpdate_page_privateMetadata | null)[]; contentJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 384c80484..3abf998d9 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -1,10 +1,15 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; +import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; import React from "react"; import { useIntl } from "react-intl"; -import PageDetailsPage from "../components/PageDetailsPage"; +import PageDetailsPage, { FormData } from "../components/PageDetailsPage"; import { TypedPageCreate } from "../mutations"; import { PageCreate as PageCreateData } from "../types/PageCreate"; import { pageListUrl, pageUrl } from "../urls"; @@ -17,6 +22,8 @@ export const PageCreate: React.FC = () => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const handlePageCreate = (data: PageCreateData) => { if (data.pageCreate.errors.length === 0) { @@ -32,41 +39,52 @@ export const PageCreate: React.FC = () => { return ( - {(pageCreate, pageCreateOpts) => ( - <> - - navigate(pageListUrl())} - onRemove={() => undefined} - onSubmit={formData => - pageCreate({ - variables: { - input: { - contentJson: JSON.stringify(formData.content), - isPublished: formData.isPublished, - publicationDate: formData.publicationDate, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - }, - slug: formData.slug === "" ? null : formData.slug, - title: formData.title - } - } - }) + {(pageCreate, pageCreateOpts) => { + const handleCreate = async (formData: FormData) => { + const result = await pageCreate({ + variables: { + input: { + contentJson: JSON.stringify(formData.content), + isPublished: formData.isPublished, + publicationDate: formData.publicationDate, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + }, + slug: formData.slug === "" ? null : formData.slug, + title: formData.title + } } - /> - - )} + }); + + return result.data.pageCreate.page?.id || null; + }; + const handleSubmit = createMetadataCreateHandler( + handleCreate, + updateMetadata, + updatePrivateMetadata + ); + + return ( + <> + + navigate(pageListUrl())} + onRemove={() => undefined} + onSubmit={handleSubmit} + /> + + ); + }} ); }; diff --git a/src/pages/views/PageDetails.tsx b/src/pages/views/PageDetails.tsx index 23c1a9c65..ded216691 100644 --- a/src/pages/views/PageDetails.tsx +++ b/src/pages/views/PageDetails.tsx @@ -4,6 +4,11 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; +import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -36,6 +41,8 @@ export const PageDetails: React.FC = ({ id, params }) => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const handlePageRemove = (data: PageRemove) => { if (data.pageDelete.errors.length === 0) { @@ -46,68 +53,82 @@ export const PageDetails: React.FC = ({ id, params }) => { navigate(pageListUrl()); } }; + return ( {(pageRemove, pageRemoveOpts) => ( {(pageUpdate, pageUpdateOpts) => ( - {pageDetails => ( - <> - pageDetails.data.page.title)} - /> - navigate(pageListUrl())} - onRemove={() => - navigate( - pageUrl(id, { - action: "remove" - }) - ) + {pageDetails => { + const handleUpdate = async (data: FormData) => { + const result = await pageUpdate({ + variables: { + id, + input: createPageInput(data) } - onSubmit={formData => - pageUpdate({ - variables: { - id, - input: createPageInput(formData) - } - }) - } - /> - navigate(pageUrl(id))} - onConfirm={pageRemove} - variant="delete" - > - - - {getStringOrPlaceholder( - pageDetails.data?.page?.title - )} - - ) - }} - /> - - - - )} + }); + + return result.data.pageUpdate.errors; + }; + + const handleSubmit = createMetadataUpdateHandler( + pageDetails.data?.page, + handleUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + + return ( + <> + pageDetails.data.page.title)} + /> + navigate(pageListUrl())} + onRemove={() => + navigate( + pageUrl(id, { + action: "remove" + }) + ) + } + onSubmit={handleSubmit} + /> + navigate(pageUrl(id))} + onConfirm={pageRemove} + variant="delete" + > + + + {getStringOrPlaceholder( + pageDetails.data?.page?.title + )} + + ) + }} + /> + + + + ); + }} )} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index c0c8b558a..26e7485fe 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -113412,6 +113412,289 @@ Ctrl + K" +
+
+
+ + Metadata + +
+
+
+
+ + + + + + + + + + + + +