diff --git a/CHANGELOG.md b/CHANGELOG.md index 36dc8e6fb..82e2bea2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ All notable, unreleased changes to this project will be documented in this file. - Fix order draft back button redirect - #753 by @orzechdev - Add manage product types and attributes permission - #768 by @orzechdev - Fix isPublished and isAvailable behaviour for products, collections and pages - #780 by @mmarkusik +- Add metadata editor to page views - #782 by @dominik-zeglen - Add missing infinite scroll to searches - #793 by @dominik-zeglen ## 2.10.1 diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index ce8015f6f..414af4209 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2901,39 +2901,6 @@ "context": "button", "string": "Add products" }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_115822814": { - "string": "No billing address" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_1161061962": { - "context": "dialog header", - "string": "Finalize Draft Order" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_1297434244": { - "string": "No user information" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_1472924390": { - "string": "There are missing or incorrect informations about this order:" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_2430492151": { - "string": "No shipping address" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_2725265632": { - "context": "button", - "string": "Finalize" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_2824936338": { - "string": "Shipping method provided, but no product requires it" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_2968256006": { - "string": "Some products require shipping, but no method provided" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_3358029330": { - "string": "Are you sure you want to finalize draft #{orderNumber}?" - }, - "src_dot_orders_dot_components_dot_OrderDraftFinalizeDialog_dot_678764806": { - "context": "button", - "string": "Finalize anyway" - }, "src_dot_orders_dot_components_dot_OrderDraftListPage_dot_2826235371": { "context": "button", "string": "Create order" 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/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index cdc16d14d..d9c551daa 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -150,7 +150,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC = props => { address, confirmButtonState, open, - errors, + errors = [], variant, - countries, + countries = [], onClose, onConfirm } = props; diff --git a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx b/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx deleted file mode 100644 index ba8ab3482..000000000 --- a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; -import ActionDialog from "@saleor/components/ActionDialog"; -import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import FormSpacer from "@saleor/components/FormSpacer"; -import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment"; -import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; -import getOrderErrorMessage from "@saleor/utils/errors/order"; -import React from "react"; -import { FormattedMessage, IntlShape, useIntl } from "react-intl"; - -export enum OrderDraftFinalizeWarning { - NO_SHIPPING, - NO_BILLING, - NO_USER, - NO_SHIPPING_METHOD, - UNNECESSARY_SHIPPING_METHOD -} - -export interface OrderDraftFinalizeDialogProps { - confirmButtonState: ConfirmButtonTransitionState; - errors: OrderErrorFragment[]; - open: boolean; - orderNumber: string; - warnings: OrderDraftFinalizeWarning[]; - onClose: () => void; - onConfirm: () => void; -} - -function translateWarnings( - intl: IntlShape -): Record { - return { - [OrderDraftFinalizeWarning.NO_BILLING]: intl.formatMessage({ - defaultMessage: "No billing address" - }), - [OrderDraftFinalizeWarning.NO_SHIPPING]: intl.formatMessage({ - defaultMessage: "No shipping address" - }), - [OrderDraftFinalizeWarning.NO_SHIPPING_METHOD]: intl.formatMessage({ - defaultMessage: "Some products require shipping, but no method provided" - }), - [OrderDraftFinalizeWarning.NO_USER]: intl.formatMessage({ - defaultMessage: "No user information" - }), - [OrderDraftFinalizeWarning.UNNECESSARY_SHIPPING_METHOD]: intl.formatMessage( - { - defaultMessage: "Shipping method provided, but no product requires it" - } - ) - }; -} - -const OrderDraftFinalizeDialog: React.FC = ({ - confirmButtonState, - errors: apiErrors, - open, - warnings, - onClose, - onConfirm, - orderNumber -}) => { - const intl = useIntl(); - const errors = useModalDialogErrors(apiErrors, open); - - const translatedWarnings = translateWarnings(intl); - - return ( - 0 - ? intl.formatMessage({ - defaultMessage: "Finalize anyway", - description: "button" - }) - : intl.formatMessage({ - defaultMessage: "Finalize", - description: "button" - }) - } - confirmButtonState={confirmButtonState} - variant={warnings.length > 0 ? "delete" : "default"} - > - - {warnings.length > 0 && ( - <> -

- -

-
    - {warnings.map(warning => ( -
  • {translatedWarnings[warning]}
  • - ))} -
- - )} - - {errors.length > 0 && ( - <> - - {errors.map((err, index) => ( - - {getOrderErrorMessage(err, intl)} - - ))} - - )} -
-
- ); -}; -OrderDraftFinalizeDialog.displayName = "OrderDraftFinalize"; -export default OrderDraftFinalizeDialog; diff --git a/src/orders/components/OrderDraftFinalizeDialog/index.ts b/src/orders/components/OrderDraftFinalizeDialog/index.ts deleted file mode 100644 index 8867a8bca..000000000 --- a/src/orders/components/OrderDraftFinalizeDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./OrderDraftFinalizeDialog"; -export * from "./OrderDraftFinalizeDialog"; diff --git a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx index 53e588717..9f8214461 100644 --- a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx +++ b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx @@ -186,13 +186,16 @@ const OrderProductAddDialog: React.FC = props => { onClose: () => setVariants([]) }); - const selectedVariantsToProductsMap = products - ? products.map(product => + const productChoices = products.filter( + product => product.variants?.length > 0 + ); + const selectedVariantsToProductsMap = productChoices + ? productChoices.map(product => product.variants.map(variant => isVariantSelected(variant, variants)) ) : []; - const productsWithAllVariantsSelected = products - ? products.map(product => + const productsWithAllVariantsSelected = productChoices + ? productChoices.map(product => hasAllVariantsSelected(product.variants, variants) ) : []; @@ -248,7 +251,7 @@ const OrderProductAddDialog: React.FC = props => { {renderCollection( - products, + productChoices, (product, productIndex) => ( diff --git a/src/orders/views/OrderDetails/OrderAddressFields.tsx b/src/orders/views/OrderDetails/OrderAddressFields.tsx new file mode 100644 index 000000000..7d823d51d --- /dev/null +++ b/src/orders/views/OrderDetails/OrderAddressFields.tsx @@ -0,0 +1,90 @@ +import { transformAddressToForm } from "@saleor/misc"; +import OrderAddressEditDialog from "@saleor/orders/components/OrderAddressEditDialog"; +import { OrderDetails } from "@saleor/orders/types/OrderDetails"; +import { + OrderDraftUpdate, + OrderDraftUpdateVariables +} from "@saleor/orders/types/OrderDraftUpdate"; +import { + OrderUpdate, + OrderUpdateVariables +} from "@saleor/orders/types/OrderUpdate"; +import { PartialMutationProviderOutput } from "@saleor/types"; +import { AddressInput } from "@saleor/types/globalTypes"; +import React from "react"; + +enum FieldType { + shipping = "shippingAddress", + billing = "billingAddress" +} + +interface Props { + action: string; + id: string; + isDraft: boolean; + data: OrderDetails; + onClose: () => void; + orderUpdate: PartialMutationProviderOutput; + orderDraftUpdate: PartialMutationProviderOutput< + OrderDraftUpdate, + OrderDraftUpdateVariables + >; +} + +const OrderAddressFields = ({ + action, + isDraft, + id, + onClose, + orderUpdate, + orderDraftUpdate, + data +}: Props) => { + const order = data?.order; + + const handleConfirm = (type: FieldType) => (value: AddressInput) => { + const updateMutation = isDraft ? orderDraftUpdate : orderUpdate; + + updateMutation.mutate({ + id, + input: { + [type]: value + } + }); + }; + + const addressFieldCommonProps = { + confirmButtonState: isDraft + ? orderDraftUpdate.opts.status + : orderUpdate.opts.status, + countries: data?.shop?.countries.map(country => ({ + code: country.code, + label: country.country + })), + errors: isDraft + ? orderDraftUpdate.opts.data?.draftOrderUpdate.errors + : orderUpdate.opts.data?.orderUpdate.errors, + onClose + }; + + return ( + <> + + + + ); +}; + +export default OrderAddressFields; diff --git a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx index fad2cd6ef..c32a5d450 100644 --- a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx +++ b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx @@ -173,6 +173,7 @@ export const OrderDetailsMessages: React.FC = ({ }) }); } + closeModal(); }; const handleShippingMethodUpdate = (data: OrderShippingMethodUpdate) => { const errs = data.orderUpdateShipping?.errors; @@ -255,7 +256,6 @@ export const OrderDetailsMessages: React.FC = ({ defaultMessage: "Draft order successfully finalized" }) }); - closeModal(); } }; const handleInvoiceGeneratePending = (data: InvoiceRequest) => { diff --git a/src/orders/views/OrderDetails/index.tsx b/src/orders/views/OrderDetails/index.tsx index fa9f48385..89482df93 100644 --- a/src/orders/views/OrderDetails/index.tsx +++ b/src/orders/views/OrderDetails/index.tsx @@ -23,25 +23,16 @@ import React from "react"; import { useIntl } from "react-intl"; import { customerUrl } from "../../../customers/urls"; -import { - getMutationState, - getStringOrPlaceholder, - maybe, - transformAddressToForm -} from "../../../misc"; +import { getMutationState, getStringOrPlaceholder, maybe } from "../../../misc"; import { productUrl } from "../../../products/urls"; import { FulfillmentStatus, JobStatusEnum, OrderStatus } from "../../../types/globalTypes"; -import OrderAddressEditDialog from "../../components/OrderAddressEditDialog"; import OrderCancelDialog from "../../components/OrderCancelDialog"; import OrderDetailsPage from "../../components/OrderDetailsPage"; import OrderDraftCancelDialog from "../../components/OrderDraftCancelDialog/OrderDraftCancelDialog"; -import OrderDraftFinalizeDialog, { - OrderDraftFinalizeWarning -} from "../../components/OrderDraftFinalizeDialog"; import OrderDraftPage from "../../components/OrderDraftPage"; import OrderFulfillmentCancelDialog from "../../components/OrderFulfillmentCancelDialog"; import OrderFulfillmentTrackingDialog from "../../components/OrderFulfillmentTrackingDialog"; @@ -52,7 +43,6 @@ import OrderProductAddDialog from "../../components/OrderProductAddDialog"; import OrderShippingMethodEditDialog from "../../components/OrderShippingMethodEditDialog"; import OrderOperations from "../../containers/OrderOperations"; import { TypedOrderDetailsQuery, useOrderVariantSearch } from "../../queries"; -import { OrderDetails_order } from "../../types/OrderDetails"; import { orderDraftListUrl, orderFulfillUrl, @@ -61,38 +51,9 @@ import { OrderUrlDialog, OrderUrlQueryParams } from "../../urls"; +import OrderAddressFields from "./OrderAddressFields"; import { OrderDetailsMessages } from "./OrderDetailsMessages"; -const orderDraftFinalizeWarnings = (order: OrderDetails_order) => { - const warnings = [] as OrderDraftFinalizeWarning[]; - if (!(order && order.shippingAddress)) { - warnings.push(OrderDraftFinalizeWarning.NO_SHIPPING); - } - if (!(order && order.billingAddress)) { - warnings.push(OrderDraftFinalizeWarning.NO_BILLING); - } - if (!(order && (order.user || order.userEmail))) { - warnings.push(OrderDraftFinalizeWarning.NO_USER); - } - if ( - order && - order.lines && - order.lines.filter(line => line.isShippingRequired).length > 0 && - order.shippingMethod === null - ) { - warnings.push(OrderDraftFinalizeWarning.NO_SHIPPING_METHOD); - } - if ( - order && - order.lines && - order.lines.filter(line => line.isShippingRequired).length === 0 && - order.shippingMethod !== null - ) { - warnings.push(OrderDraftFinalizeWarning.UNNECESSARY_SHIPPING_METHOD); - } - return warnings; -}; - interface OrderDetailsProps { id: string; params: OrderUrlQueryParams; @@ -513,7 +474,9 @@ export const OrderDetails: React.FC = ({ id, params }) => { input: data }) } - onDraftFinalize={() => openModal("finalize")} + onDraftFinalize={() => + orderDraftFinalize.mutate({ id }) + } onDraftRemove={() => openModal("cancel")} onOrderLineAdd={() => openModal("add-order-line")} onBack={() => navigate(orderDraftListUrl())} @@ -561,18 +524,6 @@ export const OrderDetails: React.FC = ({ id, params }) => { open={params.action === "cancel"} orderNumber={getStringOrPlaceholder(order?.number)} /> - orderDraftFinalize.mutate({ id })} - open={params.action === "finalize"} - orderNumber={getStringOrPlaceholder(order?.number)} - warnings={orderDraftFinalizeWarnings(order)} - /> = ({ id, params }) => { /> )} - ({ - code: country.code, - label: country.country - })) || [] - } - errors={orderUpdate.opts.data?.orderUpdate.errors || []} - open={params.action === "edit-shipping-address"} - variant="shipping" + - orderUpdate.mutate({ - id, - input: { - shippingAddress - } - }) - } - /> - ({ - code: country.code, - label: country.country - })) || [] - } - errors={orderUpdate.opts.data?.orderUpdate.errors || []} - open={params.action === "edit-billing-address"} - variant="billing" - onClose={closeModal} - onConfirm={billingAddress => - orderUpdate.mutate({ - id, - input: { - billingAddress - } - }) - } + action={params.action} /> )} 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/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index caaa33ff9..623da6239 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -199,7 +199,7 @@ export const ProductUpdatePage: React.FC = ({ const getStocksData = () => { if (product.productType.hasVariants) { - return { removeStocks: [], updateStocks: [] }; + return { addStocks: [], removeStocks: [], updateStocks: [] }; } const dataStocks = stocks.map(stock => stock.id); @@ -209,6 +209,9 @@ export const ProductUpdatePage: React.FC = ({ const stockDiff = diff(variantStocks, dataStocks); return { + addStocks: stocks.filter(stock => + stockDiff.added.some(addedStock => addedStock === stock.id) + ), removeStocks: stockDiff.removed, updateStocks: stocks.filter( stock => !stockDiff.added.some(addedStock => addedStock === stock.id) @@ -228,7 +231,6 @@ export const ProductUpdatePage: React.FC = ({ ...getAvailabilityData(data), ...getStocksData(), ...getMetadata(data), - addStocks: [], attributes }); diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index c0c8b558a..93afeac2c 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -11673,24 +11673,6 @@ exports[`Storyshots Orders / OrderDraftCancelDialog errors 1`] = ` /> `; -exports[`Storyshots Orders / OrderDraftFinalizeDialog default 1`] = ` -
-`; - -exports[`Storyshots Orders / OrderDraftFinalizeDialog with errors 1`] = ` -
-`; - -exports[`Storyshots Orders / OrderDraftFinalizeDialog with warnings 1`] = ` -
-`; - exports[`Storyshots Orders / OrderFulfillmentCancelDialog default 1`] = `
+
+
+
+ + Metadata + +
+
+
+
+ + + + + + + + + + + + +