From 7770ae34df806bc76ee6bf9bf6996a14d422e87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Fri, 28 Aug 2020 14:45:11 +0200 Subject: [PATCH] Add metadata (#670) * Add metadata editor component * Add tests * Fix plurals * Use pascal case in selectors * Update product metadata * Add metadata handler decorator * Update snapshots * wip * Remove operation provider component * Add metadata to collections * Add metadata editor to variant * Add metadata editor to categories * Add metadata to product types * Simplify code * Add metadata to attributes * Drop maybe * Rename Metadata to MetadataFragment * Update changelog and snapshots --- CHANGELOG.md | 1 + assets/images/empty-metadata.svg | 7 + locale/defaultMessages.json | 40 + .../AttributePage/AttributePage.tsx | 142 +- src/attributes/fixtures.ts | 8 + src/attributes/mutations.ts | 18 +- src/attributes/queries.ts | 3 +- src/attributes/types/AttributeCreate.ts | 14 + src/attributes/types/AttributeDetails.ts | 14 + src/attributes/types/AttributeUpdate.ts | 14 + src/attributes/types/AttributeValueCreate.ts | 14 + src/attributes/types/AttributeValueDelete.ts | 14 + src/attributes/types/AttributeValueUpdate.ts | 14 + .../views/AttributeCreate/AttributeCreate.tsx | 168 +- .../AttributeDetails/AttributeDetails.tsx | 530 +- .../views/AttributeList/AttributeList.tsx | 170 +- .../CategoryUpdatePage/CategoryUpdatePage.tsx | 268 +- src/categories/fixtures.ts | 8 + src/categories/types/CategoryCreate.ts | 14 + src/categories/types/CategoryDetails.ts | 14 + src/categories/types/CategoryUpdate.ts | 14 + src/categories/views/CategoryDetails.tsx | 52 +- .../CollectionDetailsPage.tsx | 214 +- .../containers/CollectionOperations.tsx | 113 - src/collections/fixtures.ts | 8 + src/collections/mutations.ts | 18 +- src/collections/types/CollectionDetails.ts | 14 + src/collections/types/CollectionUpdate.ts | 14 + .../types/CollectionUpdateWithHomepage.ts | 14 + src/collections/types/CreateCollection.ts | 14 + src/collections/views/CollectionCreate.tsx | 100 +- src/collections/views/CollectionDetails.tsx | 601 +- .../views/CollectionList/CollectionList.tsx | 419 +- src/components/Metadata/Metadata.stories.tsx | 19 + src/components/Metadata/Metadata.test.tsx | 162 + src/components/Metadata/Metadata.tsx | 73 + src/components/Metadata/MetadataCard.tsx | 232 + src/components/Metadata/fixtures.ts | 22 + src/components/Metadata/index.ts | 5 + src/components/Metadata/styles.ts | 66 + src/components/Metadata/types.ts | 21 + src/components/Metadata/utils.ts | 42 + src/fragments/attributes.ts | 4 + src/fragments/categories.ts | 4 + src/fragments/collections.ts | 4 + src/fragments/errors.ts | 7 + src/fragments/metadata.ts | 2 +- src/fragments/productTypes.ts | 3 + src/fragments/products.ts | 5 + .../types/AttributeDetailsFragment.ts | 14 + .../types/CategoryDetailsFragment.ts | 14 + .../types/CollectionDetailsFragment.ts | 14 + src/fragments/types/MetadataErrorFragment.ts | 15 + src/fragments/types/MetadataFragment.ts | 25 + src/fragments/types/Product.ts | 14 + .../types/ProductTypeDetailsFragment.ts | 14 + src/fragments/types/ProductVariant.ts | 14 + .../ProductTypeDetailsPage.tsx | 214 +- .../containers/ProductTypeOperations.tsx | 143 +- src/productTypes/fixtures.ts | 8 + src/productTypes/mutations.ts | 3 +- src/productTypes/types/AssignAttribute.ts | 14 + .../types/ProductTypeAttributeReorder.ts | 14 + src/productTypes/types/ProductTypeCreate.ts | 14 + src/productTypes/types/ProductTypeDetails.ts | 14 + src/productTypes/types/ProductTypeUpdate.ts | 14 + src/productTypes/types/UnassignAttribute.ts | 14 + .../views/ProductTypeUpdate/errors.tsx | 44 - .../views/ProductTypeUpdate/index.tsx | 680 +- .../ProductUpdatePage/ProductUpdatePage.tsx | 20 + .../ProductVariantPage/ProductVariantPage.tsx | 24 +- src/products/fixtures.ts | 16 + src/products/types/ProductCreate.ts | 14 + src/products/types/ProductDetails.ts | 14 + src/products/types/ProductImageCreate.ts | 14 + src/products/types/ProductImageUpdate.ts | 14 + src/products/types/ProductUpdate.ts | 14 + src/products/types/ProductVariantDetails.ts | 14 + src/products/types/SimpleProductUpdate.ts | 70 + src/products/types/VariantCreate.ts | 14 + src/products/types/VariantImageAssign.ts | 14 + src/products/types/VariantImageUnassign.ts | 14 + src/products/types/VariantUpdate.ts | 28 + src/products/utils/data.ts | 6 +- .../views/ProductUpdate/ProductUpdate.tsx | 18 +- src/products/views/ProductUpdate/handlers.ts | 42 +- src/products/views/ProductVariant.tsx | 60 +- .../__snapshots__/Stories.test.ts.snap | 8525 +++++++++++++++++ src/types/globalTypes.ts | 11 + src/utils/handlers/metadataUpdateHandler.ts | 88 + src/utils/maps.ts | 9 + src/utils/metadata/types/UpdateMetadata.ts | 62 + .../metadata/types/UpdatePrivateMetadata.ts | 62 + src/utils/metadata/updateMetadata.ts | 75 + .../metadata/useMetadataChangeTrigger.ts | 28 + testUtils/wrapper.tsx | 25 + 96 files changed, 12334 insertions(+), 2002 deletions(-) create mode 100644 assets/images/empty-metadata.svg delete mode 100644 src/collections/containers/CollectionOperations.tsx create mode 100644 src/components/Metadata/Metadata.stories.tsx create mode 100644 src/components/Metadata/Metadata.test.tsx create mode 100644 src/components/Metadata/Metadata.tsx create mode 100644 src/components/Metadata/MetadataCard.tsx create mode 100644 src/components/Metadata/fixtures.ts create mode 100644 src/components/Metadata/index.ts create mode 100644 src/components/Metadata/styles.ts create mode 100644 src/components/Metadata/types.ts create mode 100644 src/components/Metadata/utils.ts create mode 100644 src/fragments/types/MetadataErrorFragment.ts create mode 100644 src/fragments/types/MetadataFragment.ts delete mode 100644 src/productTypes/views/ProductTypeUpdate/errors.tsx create mode 100644 src/utils/handlers/metadataUpdateHandler.ts create mode 100644 src/utils/metadata/types/UpdateMetadata.ts create mode 100644 src/utils/metadata/types/UpdatePrivateMetadata.ts create mode 100644 src/utils/metadata/updateMetadata.ts create mode 100644 src/utils/metadata/useMetadataChangeTrigger.ts create mode 100644 testUtils/wrapper.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e0aa27f7b..e76500be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable, unreleased changes to this project will be documented in this file. - Fix navigator button rendering on safari browser - #656 by @dominik-zeglen - Use hooks instead of containers with render props in product mutations - #667 by @dominik-zeglen - Add apps and permission groups to navigator - #678 by @dominik-zeglen +- Add metadata - #670 by @dominik-zeglen ## 2.10.1 diff --git a/assets/images/empty-metadata.svg b/assets/images/empty-metadata.svg new file mode 100644 index 000000000..42ed7d5df --- /dev/null +++ b/assets/images/empty-metadata.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 87ef757b5..22f4dc4a3 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1549,6 +1549,46 @@ "context": "button", "string": "Add" }, + "src_dot_components_dot_Metadata_dot_1148029984": { + "context": "metadata field value, header", + "string": "Value" + }, + "src_dot_components_dot_Metadata_dot_1509432322": { + "context": "header", + "string": "Metadata" + }, + "src_dot_components_dot_Metadata_dot_1535223586": { + "context": "header", + "string": "Private Metadata" + }, + "src_dot_components_dot_Metadata_dot_2024779015": { + "context": "empty metadata text", + "string": "There is no private metadata created for this element." + }, + "src_dot_components_dot_Metadata_dot_2087058956": { + "context": "empty metadata text", + "string": "There is no metadata created for this element." + }, + "src_dot_components_dot_Metadata_dot_2537046678": { + "context": "metadata field name, header", + "string": "Field" + }, + "src_dot_components_dot_Metadata_dot_3181473584": { + "context": "add metadata field,button", + "string": "Add Field" + }, + "src_dot_components_dot_Metadata_dot_4190792473": { + "context": "table action", + "string": "Actions" + }, + "src_dot_components_dot_Metadata_dot_549104951": { + "context": "number of metadata fields in model", + "string": "{number,plural,one{{number} Field} other{{number} Fields}}" + }, + "src_dot_components_dot_Metadata_dot_553451245": { + "context": "empty metadata text", + "string": "Use the button below to add new metadata field" + }, "src_dot_components_dot_MoneyRange_dot_12301532": { "context": "money", "string": "to {money}" diff --git a/src/attributes/components/AttributePage/AttributePage.tsx b/src/attributes/components/AttributePage/AttributePage.tsx index e3ce7bc3b..c0195e699 100644 --- a/src/attributes/components/AttributePage/AttributePage.tsx +++ b/src/attributes/components/AttributePage/AttributePage.tsx @@ -4,6 +4,8 @@ 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 from "@saleor/components/Metadata/Metadata"; +import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { @@ -15,6 +17,8 @@ import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { ReorderAction } from "@saleor/types"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import React from "react"; import { useIntl } from "react-intl"; import slugify from "slugify"; @@ -38,7 +42,7 @@ export interface AttributePageProps { onValueUpdate: (id: string) => void; } -export interface AttributePageFormData { +export interface AttributePageFormData extends MetadataFormData { availableInGrid: boolean; filterableInDashboard: boolean; inputType: AttributeInputTypeEnum; @@ -65,6 +69,12 @@ const AttributePage: React.FC = ({ onValueUpdate }) => { const intl = useIntl(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const initialForm: AttributePageFormData = attribute === null ? { @@ -72,7 +82,9 @@ const AttributePage: React.FC = ({ filterableInDashboard: true, filterableInStorefront: true, inputType: AttributeInputTypeEnum.DROPDOWN, + metadata: undefined, name: "", + privateMetadata: undefined, slug: "", storefrontSearchPosition: "", valueRequired: true, @@ -92,7 +104,11 @@ const AttributePage: React.FC = ({ () => attribute.inputType, AttributeInputTypeEnum.DROPDOWN ), + metadata: attribute?.metadata?.map(mapMetadataItemToInput), name: maybe(() => attribute.name, ""), + privateMetadata: attribute?.privateMetadata?.map( + mapMetadataItemToInput + ), slug: maybe(() => attribute.slug, ""), storefrontSearchPosition: maybe( () => attribute.storefrontSearchPosition.toString(), @@ -102,66 +118,84 @@ const AttributePage: React.FC = ({ visibleInStorefront: maybe(() => attribute.visibleInStorefront, true) }; - const handleSubmit = (data: AttributePageFormData) => + const handleSubmit = (data: AttributePageFormData) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + onSubmit({ ...data, + metadata, + privateMetadata, slug: data.slug || slugify(data.name).toLowerCase() }); + }; return (
- {({ change, data, submit }) => ( - - - {intl.formatMessage(sectionNames.attributes)} - - attribute.name) - } - /> - -
- - - -
-
- -
-
- -
- )} + {({ change, data, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.attributes)} + + attribute.name) + } + /> + +
+ + + + {!!attribute && ( + <> + + + + )} +
+
+ +
+
+ +
+ ); + }}
); }; diff --git a/src/attributes/fixtures.ts b/src/attributes/fixtures.ts index bcb9405dc..67788a9fe 100644 --- a/src/attributes/fixtures.ts +++ b/src/attributes/fixtures.ts @@ -14,7 +14,15 @@ export const attribute: AttributeDetailsFragment = { filterableInStorefront: true, id: "UHJvZHVjdEF0dHJpYnV0ZTo5", inputType: AttributeInputTypeEnum.DROPDOWN, + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], name: "Author", + privateMetadata: [], slug: "author", storefrontSearchPosition: 2, valueRequired: true, diff --git a/src/attributes/mutations.ts b/src/attributes/mutations.ts index 54dd0a2cb..222f8d7b4 100644 --- a/src/attributes/mutations.ts +++ b/src/attributes/mutations.ts @@ -1,6 +1,6 @@ import { attributeDetailsFragment } from "@saleor/fragments/attributes"; import { productErrorFragment } from "@saleor/fragments/errors"; -import { TypedMutation } from "@saleor/mutations"; +import makeMutation from "@saleor/hooks/makeMutation"; import gql from "graphql-tag"; import { @@ -46,7 +46,7 @@ const attributeBulkDelete = gql` } } `; -export const AttributeBulkDeleteMutation = TypedMutation< +export const useAttributeBulkDeleteMutation = makeMutation< AttributeBulkDelete, AttributeBulkDeleteVariables >(attributeBulkDelete); @@ -61,7 +61,7 @@ const attributeDelete = gql` } } `; -export const AttributeDeleteMutation = TypedMutation< +export const useAttributeDeleteMutation = makeMutation< AttributeDelete, AttributeDeleteVariables >(attributeDelete); @@ -80,7 +80,7 @@ export const attributeUpdateMutation = gql` } } `; -export const AttributeUpdateMutation = TypedMutation< +export const useAttributeUpdateMutation = makeMutation< AttributeUpdate, AttributeUpdateVariables >(attributeUpdateMutation); @@ -99,7 +99,7 @@ const attributeValueDelete = gql` } } `; -export const AttributeValueDeleteMutation = TypedMutation< +export const useAttributeValueDeleteMutation = makeMutation< AttributeValueDelete, AttributeValueDeleteVariables >(attributeValueDelete); @@ -118,7 +118,7 @@ export const attributeValueUpdateMutation = gql` } } `; -export const AttributeValueUpdateMutation = TypedMutation< +export const useAttributeValueUpdateMutation = makeMutation< AttributeValueUpdate, AttributeValueUpdateVariables >(attributeValueUpdateMutation); @@ -137,7 +137,7 @@ export const attributeValueCreateMutation = gql` } } `; -export const AttributeValueCreateMutation = TypedMutation< +export const useAttributeValueCreateMutation = makeMutation< AttributeValueCreate, AttributeValueCreateVariables >(attributeValueCreateMutation); @@ -156,7 +156,7 @@ export const attributeCreateMutation = gql` } } `; -export const AttributeCreateMutation = TypedMutation< +export const useAttributeCreateMutation = makeMutation< AttributeCreate, AttributeCreateVariables >(attributeCreateMutation); @@ -177,7 +177,7 @@ const attributeValueReorderMutation = gql` } } `; -export const AttributeValueReorderMutation = TypedMutation< +export const useAttributeValueReorderMutation = makeMutation< AttributeValueReorder, AttributeValueReorderVariables >(attributeValueReorderMutation); diff --git a/src/attributes/queries.ts b/src/attributes/queries.ts index 1664344b9..1880de540 100644 --- a/src/attributes/queries.ts +++ b/src/attributes/queries.ts @@ -6,7 +6,6 @@ import { pageInfoFragment } from "@saleor/fragments/pageInfo"; import makeQuery from "@saleor/hooks/makeQuery"; import gql from "graphql-tag"; -import { TypedQuery } from "../queries"; import { AttributeDetails, AttributeDetailsVariables @@ -21,7 +20,7 @@ const attributeDetails = gql` } } `; -export const AttributeDetailsQuery = TypedQuery< +export const useAttributeDetailsQuery = makeQuery< AttributeDetails, AttributeDetailsVariables >(attributeDetails); diff --git a/src/attributes/types/AttributeCreate.ts b/src/attributes/types/AttributeCreate.ts index 3c7db619b..96aa56175 100644 --- a/src/attributes/types/AttributeCreate.ts +++ b/src/attributes/types/AttributeCreate.ts @@ -8,6 +8,18 @@ import { AttributeCreateInput, AttributeInputTypeEnum, AttributeValueType, Produ // GraphQL mutation operation: AttributeCreate // ==================================================== +export interface AttributeCreate_attributeCreate_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeCreate_attributeCreate_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeCreate_attributeCreate_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeCreate_attributeCreate_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeCreate_attributeCreate_attribute_metadata | null)[]; + privateMetadata: (AttributeCreate_attributeCreate_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/types/AttributeDetails.ts b/src/attributes/types/AttributeDetails.ts index f2708bb8d..1392ba7b2 100644 --- a/src/attributes/types/AttributeDetails.ts +++ b/src/attributes/types/AttributeDetails.ts @@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/global // GraphQL query operation: AttributeDetails // ==================================================== +export interface AttributeDetails_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeDetails_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeDetails_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeDetails_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeDetails_attribute_metadata | null)[]; + privateMetadata: (AttributeDetails_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/types/AttributeUpdate.ts b/src/attributes/types/AttributeUpdate.ts index 51dedb082..e06c55a1f 100644 --- a/src/attributes/types/AttributeUpdate.ts +++ b/src/attributes/types/AttributeUpdate.ts @@ -8,6 +8,18 @@ import { AttributeUpdateInput, AttributeInputTypeEnum, AttributeValueType, Produ // GraphQL mutation operation: AttributeUpdate // ==================================================== +export interface AttributeUpdate_attributeUpdate_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeUpdate_attributeUpdate_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeUpdate_attributeUpdate_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeUpdate_attributeUpdate_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeUpdate_attributeUpdate_attribute_metadata | null)[]; + privateMetadata: (AttributeUpdate_attributeUpdate_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/types/AttributeValueCreate.ts b/src/attributes/types/AttributeValueCreate.ts index 9f02505d2..83861c906 100644 --- a/src/attributes/types/AttributeValueCreate.ts +++ b/src/attributes/types/AttributeValueCreate.ts @@ -8,6 +8,18 @@ import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType, // GraphQL mutation operation: AttributeValueCreate // ==================================================== +export interface AttributeValueCreate_attributeValueCreate_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeValueCreate_attributeValueCreate_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeValueCreate_attributeValueCreate_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeValueCreate_attributeValueCreate_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeValueCreate_attributeValueCreate_attribute_metadata | null)[]; + privateMetadata: (AttributeValueCreate_attributeValueCreate_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/types/AttributeValueDelete.ts b/src/attributes/types/AttributeValueDelete.ts index 978e938b3..4f1c4c4f1 100644 --- a/src/attributes/types/AttributeValueDelete.ts +++ b/src/attributes/types/AttributeValueDelete.ts @@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./ // GraphQL mutation operation: AttributeValueDelete // ==================================================== +export interface AttributeValueDelete_attributeValueDelete_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeValueDelete_attributeValueDelete_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeValueDelete_attributeValueDelete_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeValueDelete_attributeValueDelete_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeValueDelete_attributeValueDelete_attribute_metadata | null)[]; + privateMetadata: (AttributeValueDelete_attributeValueDelete_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/types/AttributeValueUpdate.ts b/src/attributes/types/AttributeValueUpdate.ts index 564f7a691..5d653bf99 100644 --- a/src/attributes/types/AttributeValueUpdate.ts +++ b/src/attributes/types/AttributeValueUpdate.ts @@ -8,6 +8,18 @@ import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType, // GraphQL mutation operation: AttributeValueUpdate // ==================================================== +export interface AttributeValueUpdate_attributeValueUpdate_attribute_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeValueUpdate_attributeValueUpdate_attribute_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeValueUpdate_attributeValueUpdate_attribute_metadata | null)[]; + privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/attributes/views/AttributeCreate/AttributeCreate.tsx b/src/attributes/views/AttributeCreate/AttributeCreate.tsx index 1c9e5d5ff..ef09d65a0 100644 --- a/src/attributes/views/AttributeCreate/AttributeCreate.tsx +++ b/src/attributes/views/AttributeCreate/AttributeCreate.tsx @@ -1,7 +1,7 @@ import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import { maybe } from "@saleor/misc"; +import { getStringOrPlaceholder } from "@saleor/misc"; import { ReorderEvent } from "@saleor/types"; import { ProductErrorCode } from "@saleor/types/globalTypes"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; @@ -21,8 +21,7 @@ import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDia import AttributeValueEditDialog, { AttributeValueEditDialogFormData } from "../../components/AttributeValueEditDialog"; -import { AttributeCreateMutation } from "../../mutations"; -import { AttributeCreate } from "../../types/AttributeCreate"; +import { useAttributeCreateMutation } from "../../mutations"; import { attributeAddUrl, AttributeAddUrlDialog, @@ -60,6 +59,20 @@ const AttributeDetails: React.FC = ({ params }) => { [] ); + const [attributeCreate, attributeCreateOpts] = useAttributeCreateMutation({ + onCompleted: data => { + if (data.attributeCreate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Successfully created attribute" + }) + }); + navigate(attributeUrl(data.attributeCreate.attribute.id)); + } + } + }); + const id = params.id ? parseInt(params.id, 0) : undefined; const [openModal, closeModal] = createDialogActionHandlers< @@ -73,17 +86,6 @@ const AttributeDetails: React.FC = ({ params }) => { setValues(remove(values[params.id], values, areValuesEqual)); closeModal(); }; - const handleCreate = (data: AttributeCreate) => { - if (data.attributeCreate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Successfully created attribute" - }) - }); - navigate(attributeUrl(data.attributeCreate.attribute.id)); - } - }; const handleValueUpdate = (input: AttributeValueEditDialogFormData) => { if (isSelected(input, values, areValuesEqual)) { setValueErrors([attributeValueAlreadyExistsError]); @@ -104,87 +106,83 @@ const AttributeDetails: React.FC = ({ params }) => { setValues(move(values[oldIndex], values, areValuesEqual, newIndex)); return ( - - {(attributeCreate, attributeCreateOpts) => ( + <> + navigate(attributeListUrl())} + onDelete={undefined} + onSubmit={input => + attributeCreate({ + variables: { + input: { + ...input, + storefrontSearchPosition: parseInt( + input.storefrontSearchPosition, + 0 + ), + values: values.map(value => ({ + name: value.name + })) + } + } + }) + } + onValueAdd={() => openModal("add-value")} + onValueDelete={id => + openModal("remove-value", { + id + }) + } + onValueReorder={handleValueReorder} + onValueUpdate={id => + openModal("edit-value", { + id + }) + } + saveButtonBarState={attributeCreateOpts.status} + values={values.map((value, valueIndex) => ({ + __typename: "AttributeValue" as "AttributeValue", + id: valueIndex.toString(), + slug: slugify(value.name).toLowerCase(), + sortOrder: valueIndex, + type: null, + value: null, + ...value + }))} + /> + + {values.length > 0 && ( <> - navigate(attributeListUrl())} - onDelete={undefined} - onSubmit={input => - attributeCreate({ - variables: { - input: { - ...input, - storefrontSearchPosition: parseInt( - input.storefrontSearchPosition, - 0 - ), - values: values.map(value => ({ - name: value.name - })) - } - } - }) - } - onValueAdd={() => openModal("add-value")} - onValueDelete={id => - openModal("remove-value", { - id - }) - } - onValueReorder={handleValueReorder} - onValueUpdate={id => - openModal("edit-value", { - id - }) - } - saveButtonBarState={attributeCreateOpts.status} - values={values.map((value, valueIndex) => ({ - __typename: "AttributeValue" as "AttributeValue", - id: valueIndex.toString(), - slug: slugify(value.name).toLowerCase(), - sortOrder: valueIndex, - type: null, - value: null, - ...value - }))} + - {values.length > 0 && ( - <> - values[id].name, "...")} - confirmButtonState="default" - onClose={closeModal} - onConfirm={handleValueDelete} - /> - values[params.id])} - confirmButtonState="default" - disabled={false} - errors={valueErrors} - open={params.action === "edit-value"} - onClose={closeModal} - onSubmit={handleValueUpdate} - /> - - )} )} - + ); }; AttributeDetails.displayName = "AttributeDetails"; diff --git a/src/attributes/views/AttributeDetails/AttributeDetails.tsx b/src/attributes/views/AttributeDetails/AttributeDetails.tsx index 3575de0ea..b6951a2ce 100644 --- a/src/attributes/views/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/views/AttributeDetails/AttributeDetails.tsx @@ -5,29 +5,30 @@ import { maybe } from "@saleor/misc"; import { ReorderEvent } from "@saleor/types"; import { getProductErrorMessage } from "@saleor/utils/errors"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import { move } from "@saleor/utils/lists"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; import React from "react"; import { useIntl } from "react-intl"; import AttributeDeleteDialog from "../../components/AttributeDeleteDialog"; -import AttributePage from "../../components/AttributePage"; +import AttributePage, { + AttributePageFormData +} from "../../components/AttributePage"; import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog"; import AttributeValueEditDialog from "../../components/AttributeValueEditDialog"; import { - AttributeDeleteMutation, - AttributeUpdateMutation, - AttributeValueCreateMutation, - AttributeValueDeleteMutation, - AttributeValueReorderMutation, - AttributeValueUpdateMutation + useAttributeDeleteMutation, + useAttributeUpdateMutation, + useAttributeValueCreateMutation, + useAttributeValueDeleteMutation, + useAttributeValueReorderMutation, + useAttributeValueUpdateMutation } from "../../mutations"; -import { AttributeDetailsQuery } from "../../queries"; -import { AttributeDelete } from "../../types/AttributeDelete"; -import { AttributeUpdate } from "../../types/AttributeUpdate"; -import { AttributeValueCreate } from "../../types/AttributeValueCreate"; -import { AttributeValueDelete } from "../../types/AttributeValueDelete"; -import { AttributeValueReorder } from "../../types/AttributeValueReorder"; -import { AttributeValueUpdate } from "../../types/AttributeValueUpdate"; +import { useAttributeDetailsQuery } from "../../queries"; import { attributeListUrl, attributeUrl, @@ -44,285 +45,258 @@ const AttributeDetails: React.FC = ({ id, params }) => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const [openModal, closeModal] = createDialogActionHandlers< AttributeUrlDialog, AttributeUrlQueryParams >(navigate, params => attributeUrl(id, params), params); - const handleDelete = (data: AttributeDelete) => { - if (data.attributeDelete.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Attribute deleted" - }) - }); - navigate(attributeListUrl()); + const { data, loading } = useAttributeDetailsQuery({ + variables: { + id } - }; - const handleValueDelete = (data: AttributeValueDelete) => { - if (data.attributeValueDelete.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Value deleted", - description: "attribute value deleted" - }) - }); - closeModal(); + }); + + const [attributeDelete, attributeDeleteOpts] = useAttributeDeleteMutation({ + onCompleted: data => { + if (data.attributeDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Attribute deleted" + }) + }); + navigate(attributeListUrl()); + } } - }; - const handleUpdate = (data: AttributeUpdate) => { - if (data.attributeUpdate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); + }); + + const [ + attributeValueDelete, + attributeValueDeleteOpts + ] = useAttributeValueDeleteMutation({ + onCompleted: data => { + if (data.attributeValueDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Value deleted", + description: "attribute value deleted" + }) + }); + closeModal(); + } } - }; - const handleValueUpdate = (data: AttributeValueUpdate) => { - if (data.attributeValueUpdate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - closeModal(); + }); + + const [ + attributeValueUpdate, + attributeValueUpdateOpts + ] = useAttributeValueUpdateMutation({ + onCompleted: data => { + if (data.attributeValueUpdate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + } } - }; - const handleValueCreate = (data: AttributeValueCreate) => { - if (data.attributeValueCreate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Added new value", - description: "added new attribute value" - }) - }); - closeModal(); + }); + + const [attributeUpdate, attributeUpdateOpts] = useAttributeUpdateMutation({ + onCompleted: data => { + if (data.attributeUpdate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + } } - }; - const handleValueReorderMutation = (data: AttributeValueReorder) => { - if (data.attributeReorderValues.errors.length !== 0) { - notify({ - status: "error", - text: getProductErrorMessage( - data.attributeReorderValues.errors[0], - intl - ) - }); + }); + + const [ + attributeValueCreate, + attributeValueCreateOpts + ] = useAttributeValueCreateMutation({ + onCompleted: data => { + if (data.attributeValueCreate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Added new value", + description: "added new attribute value" + }) + }); + closeModal(); + } } + }); + + const [attributeValueReorder] = useAttributeValueReorderMutation({ + onCompleted: data => { + if (data.attributeReorderValues.errors.length !== 0) { + notify({ + status: "error", + text: getProductErrorMessage( + data.attributeReorderValues.errors[0], + intl + ) + }); + } + } + }); + + const handleValueReorder = ({ newIndex, oldIndex }: ReorderEvent) => + attributeValueReorder({ + optimisticResponse: { + attributeReorderValues: { + __typename: "AttributeReorderValues", + attribute: { + ...data.attribute, + values: move( + data.attribute.values[oldIndex], + data.attribute.values, + (a, b) => a.id === b.id, + newIndex + ) + }, + errors: [] + } + }, + variables: { + id, + move: { + id: data.attribute.values[oldIndex].id, + sortOrder: newIndex - oldIndex + } + } + }); + + const handleUpdate = async (data: AttributePageFormData) => { + const input = { + ...data, + inputType: undefined, + metadata: undefined, + privateMetadata: undefined, + storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 0) + }; + + const result = await attributeUpdate({ + variables: { + id, + input + } + }); + + return result.data.attributeUpdate.errors; }; + const handleSubmit = createMetadataUpdateHandler( + data?.attribute, + handleUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + return ( - - {({ data, loading }) => ( - - {(attributeDelete, attributeDeleteOpts) => ( - - {(attributeValueDelete, attributeValueDeleteOpts) => ( - - {(attributeUpdate, attributeUpdateOpts) => ( - - {(attributeValueUpdate, attributeValueUpdateOpts) => ( - - {(attributeValueCreate, attributeValueCreateOpts) => ( - - {attributeValueReorder => { - const handleValueReorder = ({ - newIndex, - oldIndex - }: ReorderEvent) => - attributeValueReorder({ - optimisticResponse: { - attributeReorderValues: { - __typename: "AttributeReorderValues", - attribute: { - ...data.attribute, - values: move( - data.attribute.values[oldIndex], - data.attribute.values, - (a, b) => a.id === b.id, - newIndex - ) - }, - errors: [] - } - }, - variables: { - id, - move: { - id: data.attribute.values[oldIndex].id, - sortOrder: newIndex - oldIndex - } - } - }); - - return ( - <> - data.attribute)} - disabled={loading} - errors={ - attributeUpdateOpts.data - ?.attributeUpdate.errors || [] - } - onBack={() => - navigate(attributeListUrl()) - } - onDelete={() => openModal("remove")} - onSubmit={data => { - const input = { - ...data, - inputType: undefined - }; - - attributeUpdate({ - variables: { - id, - input: { - ...input, - storefrontSearchPosition: parseInt( - input.storefrontSearchPosition, - 0 - ) - } - } - }); - }} - onValueAdd={() => openModal("add-value")} - onValueDelete={id => - openModal("remove-value", { - id - }) - } - onValueReorder={handleValueReorder} - onValueUpdate={id => - openModal("edit-value", { - id - }) - } - saveButtonBarState={ - attributeUpdateOpts.status - } - values={maybe( - () => data.attribute.values - )} - /> - data.attribute.name, - "..." - )} - confirmButtonState={ - attributeDeleteOpts.status - } - onClose={closeModal} - onConfirm={() => - attributeDelete({ - variables: { - id - } - }) - } - /> - data.attribute.name, - "..." - )} - open={params.action === "remove-value"} - name={maybe( - () => - data.attribute.values.find( - value => params.id === value.id - ).name, - "..." - )} - useName={true} - confirmButtonState={ - attributeValueDeleteOpts.status - } - onClose={closeModal} - onConfirm={() => - attributeValueDelete({ - variables: { - id: params.id - } - }) - } - /> - - attributeValueCreate({ - variables: { - id, - input - } - }) - } - /> - - data.attribute.values.find( - value => params.id === value.id - ) - )} - confirmButtonState={ - attributeValueUpdateOpts.status - } - disabled={loading} - errors={ - attributeValueUpdateOpts.data - ?.attributeValueUpdate.errors || [] - } - open={params.action === "edit-value"} - onClose={closeModal} - onSubmit={input => - attributeValueUpdate({ - variables: { - id: data.attribute.values.find( - value => params.id === value.id - ).id, - input - } - }) - } - /> - - ); - }} - - )} - - )} - - )} - - )} - - )} - - )} - + <> + data.attribute)} + disabled={loading} + errors={attributeUpdateOpts.data?.attributeUpdate.errors || []} + onBack={() => navigate(attributeListUrl())} + onDelete={() => openModal("remove")} + onSubmit={handleSubmit} + onValueAdd={() => openModal("add-value")} + onValueDelete={id => + openModal("remove-value", { + id + }) + } + onValueReorder={handleValueReorder} + onValueUpdate={id => + openModal("edit-value", { + id + }) + } + saveButtonBarState={attributeUpdateOpts.status} + values={maybe(() => data.attribute.values)} + /> + data.attribute.name, "...")} + confirmButtonState={attributeDeleteOpts.status} + onClose={closeModal} + onConfirm={() => + attributeDelete({ + variables: { + id + } + }) + } + /> + data.attribute.name, "...")} + open={params.action === "remove-value"} + name={maybe( + () => + data.attribute.values.find(value => params.id === value.id).name, + "..." + )} + useName={true} + confirmButtonState={attributeValueDeleteOpts.status} + onClose={closeModal} + onConfirm={() => + attributeValueDelete({ + variables: { + id: params.id + } + }) + } + /> + + attributeValueCreate({ + variables: { + id, + input + } + }) + } + /> + + data.attribute.values.find(value => params.id === value.id) + )} + confirmButtonState={attributeValueUpdateOpts.status} + disabled={loading} + errors={ + attributeValueUpdateOpts.data?.attributeValueUpdate.errors || [] + } + open={params.action === "edit-value"} + onClose={closeModal} + onSubmit={input => + attributeValueUpdate({ + variables: { + id: data.attribute.values.find(value => params.id === value.id) + .id, + input + } + }) + } + /> + ); }; AttributeDetails.displayName = "AttributeDetails"; diff --git a/src/attributes/views/AttributeList/AttributeList.tsx b/src/attributes/views/AttributeList/AttributeList.tsx index f4a65700b..d71bd2907 100644 --- a/src/attributes/views/AttributeList/AttributeList.tsx +++ b/src/attributes/views/AttributeList/AttributeList.tsx @@ -32,9 +32,8 @@ import useBulkActions from "../../../hooks/useBulkActions"; import { maybe } from "../../../misc"; import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog"; import AttributeListPage from "../../components/AttributeListPage"; -import { AttributeBulkDeleteMutation } from "../../mutations"; +import { useAttributeBulkDeleteMutation } from "../../mutations"; import { useAttributeListQuery } from "../../queries"; -import { AttributeBulkDelete } from "../../types/AttributeBulkDelete"; import { attributeAddUrl, attributeListUrl, @@ -72,6 +71,26 @@ const AttributeList: React.FC = ({ params }) => { variables: queryVariables }); + const [ + attributeBulkDelete, + attributeBulkDeleteOpts + ] = useAttributeBulkDeleteMutation({ + onCompleted: data => { + if (data.attributeBulkDelete.errors.length === 0) { + closeModal(); + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Attributes successfully delete", + description: "deleted multiple attributes" + }) + }); + reset(); + refetch(); + } + } + }); + const tabs = getFilterTabs(); const currentTab = @@ -125,96 +144,73 @@ const AttributeList: React.FC = ({ params }) => { params ); - const handleBulkDelete = (data: AttributeBulkDelete) => { - if (data.attributeBulkDelete.errors.length === 0) { - closeModal(); - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Attributes successfully delete", - description: "deleted multiple attributes" - }) - }); - reset(); - refetch(); - } - }; - const handleSort = createSortHandler(navigate, attributeListUrl, params); const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( - - {(attributeBulkDelete, attributeBulkDeleteOpts) => ( - <> - - data.attributes.edges.map(edge => edge.node) - )} - currencySymbol={currencySymbol} - currentTab={currentTab} - disabled={loading || attributeBulkDeleteOpts.loading} - filterOpts={getFilterOpts(params)} - initialSearch={params.query || ""} - isChecked={isSelected} - onAdd={() => navigate(attributeAddUrl())} - onAll={resetFilters} - onBack={() => navigate(configurationMenuUrl)} - onFilterChange={changeFilters} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - onRowClick={id => () => navigate(attributeUrl(id))} - onSearchChange={handleSearchChange} - onSort={handleSort} - onTabChange={handleTabChange} - onTabDelete={() => openModal("delete-search")} - onTabSave={() => openModal("save-search")} - pageInfo={pageInfo} - selected={listElements.length} - sort={getSortParams(params)} - tabs={tabs.map(tab => tab.name)} - toggle={toggle} - toggleAll={toggleAll} - toolbar={ - - openModal("remove", { - ids: listElements - }) - } - > - - + <> + data.attributes.edges.map(edge => edge.node))} + currencySymbol={currencySymbol} + currentTab={currentTab} + disabled={loading || attributeBulkDeleteOpts.loading} + filterOpts={getFilterOpts(params)} + initialSearch={params.query || ""} + isChecked={isSelected} + onAdd={() => navigate(attributeAddUrl())} + onAll={resetFilters} + onBack={() => navigate(configurationMenuUrl)} + onFilterChange={changeFilters} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onRowClick={id => () => navigate(attributeUrl(id))} + onSearchChange={handleSearchChange} + onSort={handleSort} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + pageInfo={pageInfo} + selected={listElements.length} + sort={getSortParams(params)} + tabs={tabs.map(tab => tab.name)} + toggle={toggle} + toggleAll={toggleAll} + toolbar={ + + openModal("remove", { + ids: listElements + }) } - /> - params.ids.length > 0) - } - onConfirm={() => - attributeBulkDelete({ variables: { ids: params.ids } }) - } - onClose={closeModal} - quantity={maybe(() => params.ids.length)} - /> - - tabs[currentTab - 1].name, "...")} - /> - - )} - + > + + + } + /> + params.ids.length > 0)} + onConfirm={() => + attributeBulkDelete({ variables: { ids: params.ids } }) + } + onClose={closeModal} + quantity={maybe(() => params.ids.length)} + /> + + tabs[currentTab - 1].name, "...")} + /> + ); }; AttributeList.displayName = "AttributeList"; diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 1e9dacdb8..7b64d842d 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -6,12 +6,16 @@ import CardTitle from "@saleor/components/CardTitle"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; +import Metadata from "@saleor/components/Metadata/Metadata"; +import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import { Tab, TabContainer } from "@saleor/components/Tab"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { sectionNames } from "@saleor/intl"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import { RawDraftContentState } from "draft-js"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -28,7 +32,7 @@ import { import CategoryBackground from "../CategoryBackground"; import CategoryProducts from "../CategoryProducts"; -export interface FormData { +export interface FormData extends MetadataFormData { backgroundImageAlt: string; description: RawDraftContentState; name: string; @@ -100,144 +104,174 @@ export const CategoryUpdatePage: React.FC = ({ toggleAll }: CategoryUpdatePageProps) => { const intl = useIntl(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const initialData: FormData = category ? { backgroundImageAlt: maybe(() => category.backgroundImage.alt, ""), description: maybe(() => JSON.parse(category.descriptionJson)), + metadata: category?.metadata?.map(mapMetadataItemToInput), name: category.name || "", + privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), seoDescription: category.seoDescription || "", seoTitle: category.seoTitle || "" } : { backgroundImageAlt: "", description: "", + metadata: undefined, name: "", + privateMetadata: undefined, seoDescription: "", seoTitle: "" }; + + const handleSubmit = (data: FormData) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + + onSubmit({ + ...data, + metadata, + privateMetadata + }); + }; + return ( -
- {({ data, change, submit, hasChanged }) => ( - - - {intl.formatMessage(sectionNames.categories)} - - - - - category.backgroundImage)} - onChange={change} - /> - - - - - - - - - - - - - {currentTab === CategoryPageTab.categories && ( - - - - - } - /> - + {({ data, change, submit, hasChanged }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.categories)} + + + + + category.backgroundImage)} + onChange={change} + /> + + + + + + + + + + + + + + + {currentTab === CategoryPageTab.categories && ( + + + + + } + /> + undefined} + /> + + )} + {currentTab === CategoryPageTab.products && ( + category.name)} + products={products} disabled={disabled} - isChecked={isChecked} - isRoot={false} pageInfo={pageInfo} - selected={selected} - sort={undefined} - toggle={toggle} - toggleAll={toggleAll} - toolbar={subcategoryListToolbar} onNextPage={onNextPage} onPreviousPage={onPreviousPage} - onRowClick={onCategoryClick} - onSort={() => undefined} + onRowClick={onProductClick} + onAdd={onAddProduct} + toggle={toggle} + toggleAll={toggleAll} + selected={selected} + isChecked={isChecked} + toolbar={productListToolbar} /> - - )} - {currentTab === CategoryPageTab.products && ( - category.name)} - products={products} - disabled={disabled} - pageInfo={pageInfo} - onNextPage={onNextPage} - onPreviousPage={onPreviousPage} - onRowClick={onProductClick} - onAdd={onAddProduct} - toggle={toggle} - toggleAll={toggleAll} - selected={selected} - isChecked={isChecked} - toolbar={productListToolbar} + )} + - )} - - - )} + + ); + }}
); }; diff --git a/src/categories/fixtures.ts b/src/categories/fixtures.ts index 9828ce611..d51e93b93 100644 --- a/src/categories/fixtures.ts +++ b/src/categories/fixtures.ts @@ -105,11 +105,19 @@ export const category: ( }, descriptionJson: JSON.stringify(content), id: "Q2F0ZWdvcnk6NA==", + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], name: "Coffees", parent: { __typename: "Category", id: "Q2F0ZWdvcnk6Mw==" }, + privateMetadata: [], products: { __typename: "ProductCountableConnection", edges: [ diff --git a/src/categories/types/CategoryCreate.ts b/src/categories/types/CategoryCreate.ts index 0bcf34a74..21991ef8d 100644 --- a/src/categories/types/CategoryCreate.ts +++ b/src/categories/types/CategoryCreate.ts @@ -8,6 +8,18 @@ import { CategoryInput, ProductErrorCode } from "./../../types/globalTypes"; // GraphQL mutation operation: CategoryCreate // ==================================================== +export interface CategoryCreate_categoryCreate_category_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CategoryCreate_categoryCreate_category_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CategoryCreate_categoryCreate_category_backgroundImage { __typename: "Image"; alt: string | null; @@ -22,6 +34,8 @@ export interface CategoryCreate_categoryCreate_category_parent { export interface CategoryCreate_categoryCreate_category { __typename: "Category"; id: string; + metadata: (CategoryCreate_categoryCreate_category_metadata | null)[]; + privateMetadata: (CategoryCreate_categoryCreate_category_privateMetadata | null)[]; backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null; name: string; descriptionJson: any; diff --git a/src/categories/types/CategoryDetails.ts b/src/categories/types/CategoryDetails.ts index 40c0869d9..5cf7baa08 100644 --- a/src/categories/types/CategoryDetails.ts +++ b/src/categories/types/CategoryDetails.ts @@ -6,6 +6,18 @@ // GraphQL query operation: CategoryDetails // ==================================================== +export interface CategoryDetails_category_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CategoryDetails_category_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CategoryDetails_category_backgroundImage { __typename: "Image"; alt: string | null; @@ -131,6 +143,8 @@ export interface CategoryDetails_category_products { export interface CategoryDetails_category { __typename: "Category"; id: string; + metadata: (CategoryDetails_category_metadata | null)[]; + privateMetadata: (CategoryDetails_category_privateMetadata | null)[]; backgroundImage: CategoryDetails_category_backgroundImage | null; name: string; descriptionJson: any; diff --git a/src/categories/types/CategoryUpdate.ts b/src/categories/types/CategoryUpdate.ts index 8b04c1583..1be777c04 100644 --- a/src/categories/types/CategoryUpdate.ts +++ b/src/categories/types/CategoryUpdate.ts @@ -8,6 +8,18 @@ import { CategoryInput, ProductErrorCode } from "./../../types/globalTypes"; // GraphQL mutation operation: CategoryUpdate // ==================================================== +export interface CategoryUpdate_categoryUpdate_category_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CategoryUpdate_categoryUpdate_category_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CategoryUpdate_categoryUpdate_category_backgroundImage { __typename: "Image"; alt: string | null; @@ -22,6 +34,8 @@ export interface CategoryUpdate_categoryUpdate_category_parent { export interface CategoryUpdate_categoryUpdate_category { __typename: "Category"; id: string; + metadata: (CategoryUpdate_categoryUpdate_category_metadata | null)[]; + privateMetadata: (CategoryUpdate_categoryUpdate_category_privateMetadata | null)[]; backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null; name: string; descriptionJson: any; diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 5dc0d750f..cb875d01d 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -12,6 +12,11 @@ import usePaginator, { } from "@saleor/hooks/usePaginator"; import { commonMessages } from "@saleor/intl"; 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 { FormattedMessage, useIntl } from "react-intl"; @@ -22,7 +27,8 @@ import { productAddUrl, productUrl } from "../../products/urls"; import { CategoryInput } from "../../types/globalTypes"; import { CategoryPageTab, - CategoryUpdatePage + CategoryUpdatePage, + FormData } from "../components/CategoryUpdatePage/CategoryUpdatePage"; import { useCategoryBulkDeleteMutation, @@ -63,6 +69,8 @@ export const CategoryDetails: React.FC = ({ params.ids ); const intl = useIntl(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const paginationState = createPaginationState(PAGINATE_BY, params); const { data, loading, refetch } = useCategoryDetailsQuery({ @@ -167,6 +175,31 @@ export const CategoryDetails: React.FC = ({ params ); + const handleUpdate = async (formData: FormData) => { + const result = await updateCategory({ + variables: { + id, + input: { + backgroundImageAlt: formData.backgroundImageAlt, + descriptionJson: JSON.stringify(formData.description), + name: formData.name, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + } + } + } + }); + + return result.data.categoryUpdate.errors; + }; + const handleSubmit = createMetadataUpdateHandler( + data?.category, + handleUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + return ( <> data.category.name)} /> @@ -209,22 +242,7 @@ export const CategoryDetails: React.FC = ({ onPreviousPage={loadPreviousPage} pageInfo={pageInfo} onProductClick={id => () => navigate(productUrl(id))} - onSubmit={formData => - updateCategory({ - variables: { - id, - input: { - backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), - name: formData.name, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - } - } - } - }) - } + onSubmit={handleSubmit} products={maybe(() => data.category.products.edges.map(edge => edge.node) )} diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index 4bdb628b3..2eb5cd0e3 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -7,6 +7,8 @@ import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import Grid from "@saleor/components/Grid"; import Hr from "@saleor/components/Hr"; +import Metadata from "@saleor/components/Metadata/Metadata"; +import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -14,6 +16,8 @@ import VisibilityCard from "@saleor/components/VisibilityCard"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; 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 { RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; @@ -25,7 +29,7 @@ import CollectionDetails from "../CollectionDetails/CollectionDetails"; import { CollectionImage } from "../CollectionImage/CollectionImage"; import CollectionProducts from "../CollectionProducts/CollectionProducts"; -export interface CollectionDetailsPageFormData { +export interface CollectionDetailsPageFormData extends MetadataFormData { backgroundImageAlt: string; description: RawDraftContentState; name: string; @@ -64,6 +68,24 @@ const CollectionDetailsPage: React.FC = ({ }: CollectionDetailsPageProps) => { const intl = useIntl(); const localizeDate = useDateLocalize(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleSubmit = (data: CollectionDetailsPageFormData) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + + onSubmit({ + ...data, + metadata, + privateMetadata + }); + }; return (
= ({ description: maybe(() => JSON.parse(collection.descriptionJson)), isFeatured, isPublished: maybe(() => collection.isPublished, false), + metadata: collection?.metadata?.map(mapMetadataItemToInput), name: maybe(() => collection.name, ""), + privateMetadata: collection?.privateMetadata?.map( + mapMetadataItemToInput + ), publicationDate: maybe(() => collection.publicationDate, ""), seoDescription: maybe(() => collection.seoDescription, ""), seoTitle: maybe(() => collection.seoTitle, "") }} - onSubmit={onSubmit} + onSubmit={handleSubmit} confirmLeave > - {({ change, data, hasChanged, submit }) => ( - - - {intl.formatMessage(sectionNames.collections)} - - collection.name)} /> - -
- - - collection.backgroundImage)} - onImageDelete={onImageDelete} - onImageUpload={onImageUpload} - onChange={change} - /> - - - - collection.name)} - onChange={change} - /> -
-
+ {({ change, data, hasChanged, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.collections)} + + collection.name)} /> +
- - -
- -
+ /> + + collection.backgroundImage)} + onImageDelete={onImageDelete} + onImageUpload={onImageUpload} + onChange={change} + /> + + + + + + collection.name)} + onChange={change} + />
-
-
- -
- )} +
+
+ + +
+ +
+
+
+ + + + ); + }} ); }; diff --git a/src/collections/containers/CollectionOperations.tsx b/src/collections/containers/CollectionOperations.tsx deleted file mode 100644 index 9a65c37f1..000000000 --- a/src/collections/containers/CollectionOperations.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; - -import { getMutationProviderData } from "../../misc"; -import { PartialMutationProviderOutput } from "../../types"; -import { - TypedCollectionAssignProductMutation, - TypedCollectionRemoveMutation, - TypedCollectionUpdateMutation, - TypedCollectionUpdateWithHomepageMutation, - TypedUnassignCollectionProductMutation -} from "../mutations"; -import { - CollectionAssignProduct, - CollectionAssignProductVariables -} from "../types/CollectionAssignProduct"; -import { - CollectionUpdate, - CollectionUpdateVariables -} from "../types/CollectionUpdate"; -import { - CollectionUpdateWithHomepage, - CollectionUpdateWithHomepageVariables -} from "../types/CollectionUpdateWithHomepage"; -import { - RemoveCollection, - RemoveCollectionVariables -} from "../types/RemoveCollection"; -import { - UnassignCollectionProduct, - UnassignCollectionProductVariables -} from "../types/UnassignCollectionProduct"; - -interface CollectionUpdateOperationsProps { - children: (props: { - updateCollectionWithHomepage: PartialMutationProviderOutput< - CollectionUpdateWithHomepage, - CollectionUpdateWithHomepageVariables - >; - assignProduct: PartialMutationProviderOutput< - CollectionAssignProduct, - CollectionAssignProductVariables - >; - unassignProduct: PartialMutationProviderOutput< - UnassignCollectionProduct, - UnassignCollectionProductVariables - >; - updateCollection: PartialMutationProviderOutput< - CollectionUpdate, - CollectionUpdateVariables - >; - removeCollection: PartialMutationProviderOutput< - RemoveCollection, - RemoveCollectionVariables - >; - }) => React.ReactNode; - onUpdate: (data: CollectionUpdate) => void; - onUpdateWithCollection: (data: CollectionUpdateWithHomepage) => void; - onProductAssign: (data: CollectionAssignProduct) => void; - onProductUnassign: (data: UnassignCollectionProduct) => void; - onRemove: (data: RemoveCollection) => void; -} - -const CollectionOperations: React.FC = ({ - children, - onUpdate, - onUpdateWithCollection, - onProductAssign, - onProductUnassign, - onRemove -}) => ( - - {(...updateCollection) => ( - - {(...removeCollection) => ( - - {(...assignProduct) => ( - - {(...updateWithHomepage) => ( - - {(...unassignProduct) => - children({ - assignProduct: getMutationProviderData( - ...assignProduct - ), - removeCollection: getMutationProviderData( - ...removeCollection - ), - unassignProduct: getMutationProviderData( - ...unassignProduct - ), - updateCollection: getMutationProviderData( - ...updateCollection - ), - updateCollectionWithHomepage: getMutationProviderData( - ...updateWithHomepage - ) - }) - } - - )} - - )} - - )} - - )} - -); -export default CollectionOperations; diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index 8fc75b09b..0119379c8 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -80,7 +80,15 @@ export const collection: ( descriptionJson: JSON.stringify(content), id: "Q29sbGVjdGlvbjox", isPublished: true, + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], name: "Summer collection", + privateMetadata: [], products: { __typename: "ProductCountableConnection", edges: [ diff --git a/src/collections/mutations.ts b/src/collections/mutations.ts index 1ed4fe5c7..bf5fafa7c 100644 --- a/src/collections/mutations.ts +++ b/src/collections/mutations.ts @@ -6,9 +6,9 @@ import { productErrorFragment, shopErrorFragment } from "@saleor/fragments/errors"; +import makeMutation from "@saleor/hooks/makeMutation"; import gql from "graphql-tag"; -import { TypedMutation } from "../mutations"; import { CollectionAssignProduct, CollectionAssignProductVariables @@ -56,7 +56,7 @@ const collectionUpdate = gql` } } `; -export const TypedCollectionUpdateMutation = TypedMutation< +export const useCollectionUpdateMutation = makeMutation< CollectionUpdate, CollectionUpdateVariables >(collectionUpdate); @@ -90,7 +90,7 @@ const collectionUpdateWithHomepage = gql` } } `; -export const TypedCollectionUpdateWithHomepageMutation = TypedMutation< +export const useCollectionUpdateWithHomepageMutation = makeMutation< CollectionUpdateWithHomepage, CollectionUpdateWithHomepageVariables >(collectionUpdateWithHomepage); @@ -129,7 +129,7 @@ const assignCollectionProduct = gql` } } `; -export const TypedCollectionAssignProductMutation = TypedMutation< +export const useCollectionAssignProductMutation = makeMutation< CollectionAssignProduct, CollectionAssignProductVariables >(assignCollectionProduct); @@ -148,7 +148,7 @@ const createCollection = gql` } } `; -export const TypedCollectionCreateMutation = TypedMutation< +export const useCollectionCreateMutation = makeMutation< CreateCollection, CreateCollectionVariables >(createCollection); @@ -163,7 +163,7 @@ const removeCollection = gql` } } `; -export const TypedCollectionRemoveMutation = TypedMutation< +export const useCollectionRemoveMutation = makeMutation< RemoveCollection, RemoveCollectionVariables >(removeCollection); @@ -213,7 +213,7 @@ const unassignCollectionProduct = gql` } } `; -export const TypedUnassignCollectionProductMutation = TypedMutation< +export const useUnassignCollectionProductMutation = makeMutation< UnassignCollectionProduct, UnassignCollectionProductVariables >(unassignCollectionProduct); @@ -228,7 +228,7 @@ const collectionBulkDelete = gql` } } `; -export const TypedCollectionBulkDelete = TypedMutation< +export const useCollectionBulkDelete = makeMutation< CollectionBulkDelete, CollectionBulkDeleteVariables >(collectionBulkDelete); @@ -243,7 +243,7 @@ const collectionBulkPublish = gql` } } `; -export const TypedCollectionBulkPublish = TypedMutation< +export const useCollectionBulkPublish = makeMutation< CollectionBulkPublish, CollectionBulkPublishVariables >(collectionBulkPublish); diff --git a/src/collections/types/CollectionDetails.ts b/src/collections/types/CollectionDetails.ts index 4f24c83ce..f46367d6a 100644 --- a/src/collections/types/CollectionDetails.ts +++ b/src/collections/types/CollectionDetails.ts @@ -6,6 +6,18 @@ // GraphQL query operation: CollectionDetails // ==================================================== +export interface CollectionDetails_collection_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CollectionDetails_collection_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CollectionDetails_collection_backgroundImage { __typename: "Image"; alt: string | null; @@ -56,6 +68,8 @@ export interface CollectionDetails_collection { id: string; isPublished: boolean; name: string; + metadata: (CollectionDetails_collection_metadata | null)[]; + privateMetadata: (CollectionDetails_collection_privateMetadata | null)[]; backgroundImage: CollectionDetails_collection_backgroundImage | null; descriptionJson: any; publicationDate: any | null; diff --git a/src/collections/types/CollectionUpdate.ts b/src/collections/types/CollectionUpdate.ts index 42f43cfd5..a3b85081f 100644 --- a/src/collections/types/CollectionUpdate.ts +++ b/src/collections/types/CollectionUpdate.ts @@ -8,6 +8,18 @@ import { CollectionInput, ProductErrorCode } from "./../../types/globalTypes"; // GraphQL mutation operation: CollectionUpdate // ==================================================== +export interface CollectionUpdate_collectionUpdate_collection_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CollectionUpdate_collectionUpdate_collection_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CollectionUpdate_collectionUpdate_collection_backgroundImage { __typename: "Image"; alt: string | null; @@ -19,6 +31,8 @@ export interface CollectionUpdate_collectionUpdate_collection { id: string; isPublished: boolean; name: string; + metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[]; + privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[]; backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null; descriptionJson: any; publicationDate: any | null; diff --git a/src/collections/types/CollectionUpdateWithHomepage.ts b/src/collections/types/CollectionUpdateWithHomepage.ts index a51f43b1e..d034e7cfe 100644 --- a/src/collections/types/CollectionUpdateWithHomepage.ts +++ b/src/collections/types/CollectionUpdateWithHomepage.ts @@ -30,6 +30,18 @@ export interface CollectionUpdateWithHomepage_homepageCollectionUpdate { shop: CollectionUpdateWithHomepage_homepageCollectionUpdate_shop | null; } +export interface CollectionUpdateWithHomepage_collectionUpdate_collection_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage { __typename: "Image"; alt: string | null; @@ -41,6 +53,8 @@ export interface CollectionUpdateWithHomepage_collectionUpdate_collection { id: string; isPublished: boolean; name: string; + metadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_metadata | null)[]; + privateMetadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata | null)[]; backgroundImage: CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage | null; descriptionJson: any; publicationDate: any | null; diff --git a/src/collections/types/CreateCollection.ts b/src/collections/types/CreateCollection.ts index 2a5ea5d9d..7f1646436 100644 --- a/src/collections/types/CreateCollection.ts +++ b/src/collections/types/CreateCollection.ts @@ -8,6 +8,18 @@ import { CollectionCreateInput, ProductErrorCode } from "./../../types/globalTyp // GraphQL mutation operation: CreateCollection // ==================================================== +export interface CreateCollection_collectionCreate_collection_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CreateCollection_collectionCreate_collection_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CreateCollection_collectionCreate_collection_backgroundImage { __typename: "Image"; alt: string | null; @@ -19,6 +31,8 @@ export interface CreateCollection_collectionCreate_collection { id: string; isPublished: boolean; name: string; + metadata: (CreateCollection_collectionCreate_collection_metadata | null)[]; + privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[]; backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null; descriptionJson: any; publicationDate: any | null; diff --git a/src/collections/views/CollectionCreate.tsx b/src/collections/views/CollectionCreate.tsx index 47a3c99ba..6e90baf6b 100644 --- a/src/collections/views/CollectionCreate.tsx +++ b/src/collections/views/CollectionCreate.tsx @@ -7,8 +7,7 @@ import { useIntl } from "react-intl"; import { CollectionCreateInput } from "../../types/globalTypes"; import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage"; -import { TypedCollectionCreateMutation } from "../mutations"; -import { CreateCollection } from "../types/CreateCollection"; +import { useCollectionCreateMutation } from "../mutations"; import { collectionListUrl, collectionUrl } from "../urls"; export const CollectionCreate: React.FC = () => { @@ -16,62 +15,61 @@ export const CollectionCreate: React.FC = () => { const notify = useNotifier(); const intl = useIntl(); - const handleCollectionCreateSuccess = (data: CreateCollection) => { - if (data.collectionCreate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - navigate(collectionUrl(data.collectionCreate.collection.id)); - } else { - const backgroundImageError = data.collectionCreate.errors.find( - error => - error.field === ("backgroundImage" as keyof CollectionCreateInput) - ); - if (backgroundImageError) { + const [createCollection, createCollectionOpts] = useCollectionCreateMutation({ + onCompleted: data => { + if (data.collectionCreate.errors.length === 0) { notify({ - status: "error", - text: intl.formatMessage(commonMessages.somethingWentWrong) + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) }); + navigate(collectionUrl(data.collectionCreate.collection.id)); + } else { + const backgroundImageError = data.collectionCreate.errors.find( + error => + error.field === ("backgroundImage" as keyof CollectionCreateInput) + ); + if (backgroundImageError) { + notify({ + status: "error", + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + } } } - }; + }); + return ( - - {(createCollection, createCollectionOpts) => ( - <> - - navigate(collectionListUrl())} - disabled={createCollectionOpts.loading} - onSubmit={formData => - createCollection({ - variables: { - input: { - backgroundImage: formData.backgroundImage.value, - backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), - isPublished: formData.isPublished, - name: formData.name, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - } - } + <> + + navigate(collectionListUrl())} + disabled={createCollectionOpts.loading} + onSubmit={formData => + createCollection({ + variables: { + input: { + backgroundImage: formData.backgroundImage.value, + backgroundImageAlt: formData.backgroundImageAlt, + descriptionJson: JSON.stringify(formData.description), + isPublished: formData.isPublished, + name: formData.name, + seo: { + description: formData.seoDescription, + title: formData.seoTitle } - }) + } } - saveButtonBarState={createCollectionOpts.status} - /> - - )} - + }) + } + saveButtonBarState={createCollectionOpts.status} + /> + ); }; export default CollectionCreate; diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 98e53ebc5..968238155 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -14,6 +14,11 @@ import usePaginator, { import { commonMessages } from "@saleor/intl"; import useProductSearch from "@saleor/searches/useProductSearch"; 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 { FormattedMessage, useIntl } from "react-intl"; @@ -23,13 +28,15 @@ import { CollectionInput } from "../../types/globalTypes"; import CollectionDetailsPage, { CollectionDetailsPageFormData } from "../components/CollectionDetailsPage/CollectionDetailsPage"; -import CollectionOperations from "../containers/CollectionOperations"; +import { + useCollectionAssignProductMutation, + useCollectionRemoveMutation, + useCollectionUpdateMutation, + useCollectionUpdateWithHomepageMutation, + useUnassignCollectionProductMutation +} from "../mutations"; import { TypedCollectionDetailsQuery } from "../queries"; -import { CollectionAssignProduct } from "../types/CollectionAssignProduct"; import { CollectionUpdate } from "../types/CollectionUpdate"; -import { CollectionUpdateWithHomepage } from "../types/CollectionUpdateWithHomepage"; -import { RemoveCollection } from "../types/RemoveCollection"; -import { UnassignCollectionProduct } from "../types/UnassignCollectionProduct"; import { collectionListUrl, collectionUrl, @@ -56,6 +63,90 @@ export const CollectionDetails: React.FC = ({ const { search, result } = useProductSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); + + const handleCollectionUpdate = (data: CollectionUpdate) => { + if (data.collectionUpdate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(collectionUrl(id)); + } else { + const backgroundImageError = data.collectionUpdate.errors.find( + error => error.field === ("backgroundImage" as keyof CollectionInput) + ); + if (backgroundImageError) { + notify({ + status: "error", + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + } + } + }; + const [updateCollection, updateCollectionOpts] = useCollectionUpdateMutation({ + onCompleted: handleCollectionUpdate + }); + + const [ + updateCollectionWithHomepage, + updateCollectionWithHomepageOpts + ] = useCollectionUpdateWithHomepageMutation({ + onCompleted: data => { + if (data.homepageCollectionUpdate.errors.length === 0) { + handleCollectionUpdate(data); + } + } + }); + + const [assignProduct, assignProductOpts] = useCollectionAssignProductMutation( + { + onCompleted: data => { + if (data.collectionAddProducts.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Added product to collection" + }) + }); + navigate(collectionUrl(id), true); + } + } + } + ); + + const [ + unassignProduct, + unassignProductOpts + ] = useUnassignCollectionProductMutation({ + onCompleted: data => { + if (data.collectionRemoveProducts.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Deleted product from collection" + }) + }); + reset(); + closeModal(); + } + } + }); + + const [removeCollection, removeCollectionOpts] = useCollectionRemoveMutation({ + onCompleted: data => { + if (data.collectionDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Deleted collection" + }) + }); + navigate(collectionListUrl()); + } + } + }); const [openModal, closeModal] = createDialogActionHandlers< CollectionUrlDialog, @@ -76,298 +167,236 @@ export const CollectionDetails: React.FC = ({ if (collection === null) { return ; } - const handleCollectionUpdate = (data: CollectionUpdate) => { - if (data.collectionUpdate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - navigate(collectionUrl(id)); - } else { - const backgroundImageError = data.collectionUpdate.errors.find( - error => - error.field === ("backgroundImage" as keyof CollectionInput) - ); - if (backgroundImageError) { - notify({ - status: "error", - text: intl.formatMessage(commonMessages.somethingWentWrong) - }); - } - } - }; - const handleCollectioUpdateWithHomepage = ( - data: CollectionUpdateWithHomepage + + const handleUpdate = async ( + formData: CollectionDetailsPageFormData ) => { - if (data.homepageCollectionUpdate.errors.length === 0) { - handleCollectionUpdate(data); - } - }; + const input: CollectionInput = { + backgroundImageAlt: formData.backgroundImageAlt, + descriptionJson: JSON.stringify(formData.description), + isPublished: formData.isPublished, + name: formData.name, + publicationDate: formData.publicationDate, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + } + }; + const isFeatured = data.shop.homepageCollection + ? data.shop.homepageCollection.id === data.collection.id + : false; - const handleProductAssign = (data: CollectionAssignProduct) => { - if (data.collectionAddProducts.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Added product to collection" - }) + if (formData.isFeatured !== isFeatured) { + const result = await updateCollectionWithHomepage({ + variables: { + homepageId: formData.isFeatured ? id : null, + id, + input + } + }); + return [ + ...result.data.collectionUpdate.errors, + ...result.data.homepageCollectionUpdate.errors + ]; + } else { + const result = await updateCollection({ + variables: { + id, + input + } }); - navigate(collectionUrl(id), true); - } - }; - const handleProductUnassign = (data: UnassignCollectionProduct) => { - if (data.collectionRemoveProducts.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Deleted product from collection" - }) - }); - reset(); - closeModal(); + return result.data.collectionUpdate.errors; } }; + const handleSubmit = createMetadataUpdateHandler( + data?.collection, + handleUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + + const formTransitionState = getMutationState( + updateCollectionOpts.called || + updateCollectionWithHomepageOpts.called, + updateCollectionOpts.loading || + updateCollectionWithHomepageOpts.loading, + updateCollectionOpts.data?.collectionUpdate.errors, + updateCollectionWithHomepageOpts.data?.collectionUpdate.errors, + updateCollectionWithHomepageOpts.data?.homepageCollectionUpdate.errors + ); + + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + data?.collection?.products?.pageInfo, + paginationState, + params + ); - const handleCollectionRemove = (data: RemoveCollection) => { - if (data.collectionDelete.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Deleted collection" - }) - }); - navigate(collectionListUrl()); - } - }; return ( - - {({ - updateCollection, - updateCollectionWithHomepage, - assignProduct, - unassignProduct, - removeCollection - }) => { - const handleSubmit = ( - formData: CollectionDetailsPageFormData - ) => { - const input: CollectionInput = { - backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), - isPublished: formData.isPublished, - name: formData.name, - publicationDate: formData.publicationDate, - seo: { - description: formData.seoDescription, - title: formData.seoTitle + <> + data.collection.name)} /> + openModal("assign")} + onBack={handleBack} + disabled={loading} + collection={data?.collection} + errors={updateCollectionOpts?.data?.collectionUpdate.errors || []} + isFeatured={maybe( + () => data.shop.homepageCollection.id === data.collection.id, + false + )} + onCollectionRemove={() => openModal("remove")} + onImageDelete={() => openModal("removeImage")} + onImageUpload={file => + updateCollection({ + variables: { + id, + input: { + backgroundImage: file + } } - }; - const isFeatured = data.shop.homepageCollection - ? data.shop.homepageCollection.id === data.collection.id - : false; - - if (formData.isFeatured !== isFeatured) { - updateCollectionWithHomepage.mutate({ - homepageId: formData.isFeatured ? id : null, - id, - input - }); - } else { - updateCollection.mutate({ - id, - input - }); - } - }; - - const formTransitionState = getMutationState( - updateCollection.opts.called || - updateCollectionWithHomepage.opts.called, - updateCollection.opts.loading || - updateCollectionWithHomepage.opts.loading, - maybe(() => updateCollection.opts.data.collectionUpdate.errors), - maybe( - () => - updateCollectionWithHomepage.opts.data.collectionUpdate - .errors - ), - maybe( - () => - updateCollectionWithHomepage.opts.data - .homepageCollectionUpdate.errors - ) - ); - - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - maybe(() => data.collection.products.pageInfo), - paginationState, - params - ); - - return ( - <> - data.collection.name)} /> - openModal("assign")} - onBack={handleBack} - disabled={loading} - collection={data?.collection} - errors={ - updateCollection.opts?.data?.collectionUpdate.errors || [] - } - isFeatured={maybe( - () => - data.shop.homepageCollection.id === data.collection.id, - false - )} - onCollectionRemove={() => openModal("remove")} - onImageDelete={() => openModal("removeImage")} - onImageUpload={file => - updateCollection.mutate({ - id, - input: { - backgroundImage: file - } - }) - } - onSubmit={handleSubmit} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - pageInfo={pageInfo} - onProductUnassign={(productId, event) => { - event.stopPropagation(); - unassignProduct.mutate({ - collectionId: id, - productIds: [productId], - ...paginationState - }); - }} - onRowClick={id => () => navigate(productUrl(id))} - saveButtonBarState={formTransitionState} - toolbar={ - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} + }) + } + onSubmit={handleSubmit} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + pageInfo={pageInfo} + onProductUnassign={(productId, event) => { + event.stopPropagation(); + unassignProduct({ + variables: { + collectionId: id, + productIds: [productId], + ...paginationState + } + }); + }} + onRowClick={id => () => navigate(productUrl(id))} + saveButtonBarState={formTransitionState} + toolbar={ + + } + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + /> + + assignProduct({ + variables: { + ...paginationState, + collectionId: id, + productIds: products.map(product => product.id) + } + }) + } + products={maybe(() => + result.data.search.edges + .map(edge => edge.node) + .filter(suggestedProduct => suggestedProduct.id) + )} + /> + + removeCollection({ + variables: { id } + }) + } + open={params.action === "remove"} + title={intl.formatMessage({ + defaultMessage: "Delete Collection", + description: "dialog title" + })} + variant="delete" + > + + + {maybe(() => data.collection.name, "...")} + + ) + }} + /> + + + + unassignProduct({ + variables: { + ...paginationState, + collectionId: id, + productIds: params.ids + } + }) + } + open={params.action === "unassign"} + title={intl.formatMessage({ + defaultMessage: "Unassign products from collection", + description: "dialog title" + })} + > + + params.ids.length), + displayQuantity: ( + {maybe(() => params.ids.length)} + ) + }} + /> + + + + updateCollection({ + variables: { + id, + input: { + backgroundImage: null } - products={maybe(() => - result.data.search.edges - .map(edge => edge.node) - .filter(suggestedProduct => suggestedProduct.id) - )} - /> - removeCollection.mutate({ id })} - open={params.action === "remove"} - title={intl.formatMessage({ - defaultMessage: "Delete Collection", - description: "dialog title" - })} - variant="delete" - > - - - {maybe(() => data.collection.name, "...")} - - ) - }} - /> - - - - unassignProduct.mutate({ - ...paginationState, - collectionId: id, - productIds: params.ids - }) - } - open={params.action === "unassign"} - title={intl.formatMessage({ - defaultMessage: "Unassign products from collection", - description: "dialog title" - })} - > - - params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ) - }} - /> - - - - updateCollection.mutate({ - id, - input: { - backgroundImage: null - } - }) - } - open={params.action === "removeImage"} - title={intl.formatMessage({ - defaultMessage: "Delete image", - description: "dialog title" - })} - variant="delete" - > - - - - - - ); - }} - + } + }) + } + open={params.action === "removeImage"} + title={intl.formatMessage({ + defaultMessage: "Delete image", + description: "dialog title" + })} + variant="delete" + > + + + + + ); }} diff --git a/src/collections/views/CollectionList/CollectionList.tsx b/src/collections/views/CollectionList/CollectionList.tsx index 92ba8272f..37693e38c 100644 --- a/src/collections/views/CollectionList/CollectionList.tsx +++ b/src/collections/views/CollectionList/CollectionList.tsx @@ -27,12 +27,10 @@ import { FormattedMessage, useIntl } from "react-intl"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; import { - TypedCollectionBulkDelete, - TypedCollectionBulkPublish + useCollectionBulkDelete, + useCollectionBulkPublish } from "../../mutations"; import { useCollectionListQuery } from "../../queries"; -import { CollectionBulkDelete } from "../../types/CollectionBulkDelete"; -import { CollectionBulkPublish } from "../../types/CollectionBulkPublish"; import { collectionAddUrl, collectionListUrl, @@ -83,6 +81,40 @@ export const CollectionList: React.FC = ({ params }) => { variables: queryVariables }); + const [ + collectionBulkDelete, + collectionBulkDeleteOpts + ] = useCollectionBulkDelete({ + onCompleted: data => { + if (data.collectionBulkDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + refetch(); + reset(); + closeModal(); + } + } + }); + + const [ + collectionBulkPublish, + collectionBulkPublishOpts + ] = useCollectionBulkPublish({ + onCompleted: data => { + if (data.collectionBulkPublish.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + refetch(); + reset(); + closeModal(); + } + } + }); + const tabs = getFilterTabs(); const currentTab = @@ -136,225 +168,178 @@ export const CollectionList: React.FC = ({ params }) => { params ); - const handleCollectionBulkDelete = (data: CollectionBulkDelete) => { - if (data.collectionBulkDelete.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - refetch(); - reset(); - closeModal(); - } - }; - - const handleCollectionBulkPublish = (data: CollectionBulkPublish) => { - if (data.collectionBulkPublish.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - refetch(); - reset(); - closeModal(); - } - }; - const handleSort = createSortHandler(navigate, collectionListUrl, params); const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( - - {(collectionBulkDelete, collectionBulkDeleteOpts) => ( - - {(collectionBulkPublish, collectionBulkPublishOpts) => ( - <> - navigate(collectionAddUrl)} - onAll={resetFilters} - onTabChange={handleTabChange} - onTabDelete={() => openModal("delete-search")} - onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} - disabled={loading} - collections={maybe(() => - data.collections.edges.map(edge => edge.node) - )} - settings={settings} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - onSort={handleSort} - onUpdateListSettings={updateListSettings} - pageInfo={pageInfo} - sort={getSortParams(params)} - onRowClick={id => () => navigate(collectionUrl(id))} - toolbar={ - <> - - - - openModal("remove", { - ids: listElements - }) - } - > - - - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} + <> + navigate(collectionAddUrl)} + onAll={resetFilters} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} + disabled={loading} + collections={maybe(() => data.collections.edges.map(edge => edge.node))} + settings={settings} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onSort={handleSort} + onUpdateListSettings={updateListSettings} + pageInfo={pageInfo} + sort={getSortParams(params)} + onRowClick={id => () => navigate(collectionUrl(id))} + toolbar={ + <> + + + openModal("remove", { + ids: listElements + }) + } + > + + + + } + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + /> + params.ids.length > 0)} + onClose={closeModal} + confirmButtonState={collectionBulkPublishOpts.status} + onConfirm={() => + collectionBulkPublish({ + variables: { + ids: params.ids, + isPublished: true + } + }) + } + variant="default" + title={intl.formatMessage({ + defaultMessage: "Publish collections", + description: "dialog title" + })} + > + + params.ids.length), + displayQuantity: {maybe(() => params.ids.length)} + }} + /> + + + params.ids.length > 0) + } + onClose={closeModal} + confirmButtonState={collectionBulkPublishOpts.status} + onConfirm={() => + collectionBulkPublish({ + variables: { + ids: params.ids, + isPublished: false + } + }) + } + variant="default" + title={intl.formatMessage({ + defaultMessage: "Unpublish collections", + description: "dialog title" + })} + > + + params.ids.length), + displayQuantity: {maybe(() => params.ids.length)} + }} + /> + + + params.ids.length > 0)} + onClose={closeModal} + confirmButtonState={collectionBulkDeleteOpts.status} + onConfirm={() => + collectionBulkDelete({ + variables: { + ids: params.ids + } + }) + } + variant="delete" + title={intl.formatMessage({ + defaultMessage: "Delete collections", + description: "dialog title" + })} + > + + params.ids.length), + displayQuantity: {maybe(() => params.ids.length)} + }} + /> + + + + tabs[currentTab - 1].name, "...")} + /> + ); }; export default CollectionList; diff --git a/src/components/Metadata/Metadata.stories.tsx b/src/components/Metadata/Metadata.stories.tsx new file mode 100644 index 000000000..f60c6ac6c --- /dev/null +++ b/src/components/Metadata/Metadata.stories.tsx @@ -0,0 +1,19 @@ +import useForm from "@saleor/hooks/useForm"; +import Decorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { props } from "./fixtures"; +import Metadata from "./Metadata"; + +const InteractiveStory: React.FC = () => { + const { change, data } = useForm(props.data, () => undefined); + + return ; +}; + +storiesOf("Generics / Metadata", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ) + .add("interactive", () => ); diff --git a/src/components/Metadata/Metadata.test.tsx b/src/components/Metadata/Metadata.test.tsx new file mode 100644 index 000000000..8cb49c4eb --- /dev/null +++ b/src/components/Metadata/Metadata.test.tsx @@ -0,0 +1,162 @@ +import useForm from "@saleor/hooks/useForm"; +import Wrapper from "@test/wrapper"; +import { configure } from "enzyme"; +import { mount } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +import React from "react"; + +import { props } from "./fixtures"; +import Metadata from "./Metadata"; + +configure({ adapter: new Adapter() }); + +const expandButton = 'data-test="expand"'; + +const Component: React.FC = () => { + const { change, data } = useForm(props.data, jest.fn()); + + return ( + + + + ); +}; + +describe("Metadata editor", () => { + it("can expand field", () => { + const wrapper = mount(); + + const expandDataEl = "data-test-expanded"; + + expect( + wrapper + .find(`[${expandDataEl}]`) + .first() + .prop(expandDataEl) + ).toEqual(false); + wrapper + .find(`[${expandButton}]`) + .first() + .simulate("click"); + expect( + wrapper + .find(`[${expandDataEl}]`) + .first() + .prop(expandDataEl) + ).toEqual(true); + }); + + it("can edit field name", () => { + const wrapper = mount(); + + const inputNameSelector = '[name="name:1"] input'; + + // Expand to reveal fields + wrapper + .find(`[${expandButton}]`) + .first() + .simulate("click"); + + expect( + wrapper + .find(inputNameSelector) + .first() + .prop("value") + ).toEqual(props.data.metadata[1].key); + + wrapper + .find(inputNameSelector) + .first() + .simulate("change", { target: { name: "name:1", value: "x" } }); + + expect( + wrapper + .find(inputNameSelector) + .first() + .prop("value") + ).toEqual("x"); + }); + + it("can edit field value", () => { + const wrapper = mount(); + + const inputNameSelector = '[name="value:1"] textarea'; + + // Expand to reveal fields + wrapper + .find(`[${expandButton}]`) + .first() + .simulate("click"); + + expect( + wrapper + .find(inputNameSelector) + .first() + .prop("value") + ).toEqual(props.data.metadata[1].value); + + wrapper + .find(inputNameSelector) + .first() + .simulate("change", { target: { name: "value:1", value: "x" } }); + + expect( + wrapper + .find(inputNameSelector) + .first() + .prop("value") + ).toEqual("x"); + }); + + it("can delete field", () => { + const wrapper = mount(); + + const fieldSelector = 'tr[data-test="field"]'; + const deleteButtonSelector = '[data-test="deleteField"]'; + + // Expand to reveal fields + wrapper + .find(`[${expandButton}]`) + .first() + .simulate("click"); + + expect(wrapper.find(fieldSelector).length).toEqual( + props.data.metadata.length + ); + + wrapper + .find(deleteButtonSelector) + .first() + .simulate("click"); + + expect(wrapper.find(fieldSelector).length).toEqual( + props.data.metadata.length - 1 + ); + }); + + it("can add field", () => { + const wrapper = mount(); + + const fieldSelector = 'tr[data-test="field"]'; + const addButtonSelector = '[data-test="addField"]'; + + // Expand to reveal fields + wrapper + .find(`[${expandButton}]`) + .first() + .simulate("click"); + + expect(wrapper.find(fieldSelector).length).toEqual( + props.data.metadata.length + ); + + wrapper + .find(addButtonSelector) + .first() + .simulate("click"); + + expect(wrapper.find(fieldSelector).length).toEqual( + props.data.metadata.length + 1 + ); + }); +}); diff --git a/src/components/Metadata/Metadata.tsx b/src/components/Metadata/Metadata.tsx new file mode 100644 index 000000000..67a496bf5 --- /dev/null +++ b/src/components/Metadata/Metadata.tsx @@ -0,0 +1,73 @@ +import { ChangeEvent } from "@saleor/hooks/useForm"; +import { MetadataInput } from "@saleor/types/globalTypes"; +import { removeAtIndex, updateAtIndex } from "@saleor/utils/lists"; +import React from "react"; + +import CardSpacer from "../CardSpacer"; +import MetadataCard, { MetadataCardProps } from "./MetadataCard"; +import { EventDataAction, EventDataField } from "./types"; +import { getDataKey, parseEventData } from "./utils"; + +export interface MetadataProps + extends Omit { + data: Record<"metadata" | "privateMetadata", MetadataInput[]>; +} + +const Metadata: React.FC = ({ data, onChange }) => { + const change = (event: ChangeEvent, isPrivate: boolean) => { + const { action, field, fieldIndex, value } = parseEventData(event); + const key = getDataKey(isPrivate); + const dataToUpdate = data[key]; + + onChange({ + target: { + name: key, + value: + action === EventDataAction.update + ? updateAtIndex( + { + ...dataToUpdate[fieldIndex], + key: + field === EventDataField.name + ? value + : dataToUpdate[fieldIndex].key, + value: + field === EventDataField.value + ? value + : dataToUpdate[fieldIndex].value + }, + dataToUpdate, + fieldIndex + ) + : action === EventDataAction.add + ? [ + ...dataToUpdate, + { + key: "", + value: "" + } + ] + : removeAtIndex(dataToUpdate, fieldIndex) + } + }); + }; + + return ( + <> + change(event, false)} + /> + + change(event, true)} + /> + + ); +}; + +Metadata.displayName = "Metadata"; +export default Metadata; diff --git a/src/components/Metadata/MetadataCard.tsx b/src/components/Metadata/MetadataCard.tsx new file mode 100644 index 000000000..63b520c77 --- /dev/null +++ b/src/components/Metadata/MetadataCard.tsx @@ -0,0 +1,232 @@ +import emptyMetadata from "@assets/images/empty-metadata.svg"; +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import CardActions from "@material-ui/core/CardActions"; +import CardContent from "@material-ui/core/CardContent"; +import IconButton from "@material-ui/core/IconButton"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import ToggleIcon from "@material-ui/icons/ArrowDropDown"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { FormChange } from "@saleor/hooks/useForm"; +import { MetadataInput } from "@saleor/types/globalTypes"; +import React, { useEffect } from "react"; +import SVG from "react-inlinesvg"; +import { useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; + +import CardTitle from "../CardTitle"; +import Skeleton from "../Skeleton"; +import useStyles from "./styles"; +import { EventDataAction, EventDataField } from "./types"; + +export interface MetadataCardProps { + data: MetadataInput[]; + isPrivate: boolean; + onChange: FormChange; +} + +export const nameSeparator = ":"; +export const nameInputPrefix = EventDataField.name; +export const valueInputPrefix = EventDataField.value; + +const MetadataCard: React.FC = ({ + data, + isPrivate, + onChange +}) => { + const intl = useIntl(); + const loaded = React.useRef(false); + const [expanded, setExpanded] = React.useState(true); + const classes = useStyles({}); + + useEffect(() => { + if (data !== undefined) { + loaded.current = true; + if (data.length > 0) { + setExpanded(false); + } + } + }, [data === undefined]); + + return ( + + + {data === undefined ? ( + + + + ) : ( + <> + + {data.length > 0 && ( +
+ + + + setExpanded(!expanded)} + > + + +
+ )} +
+ {expanded && ( + <> + {data.length === 0 ? ( +
+ + + {isPrivate ? ( + + ) : ( + + )} + + + + +
+ ) : ( + + + + + + + + + + + + + + + + {data.map((field, fieldIndex) => ( + + + + + + + + + + onChange({ + target: { + name: EventDataAction.delete, + value: fieldIndex + } + }) + } + > + + + + + ))} + +
+ )} + + + + + )} + + )} +
+ ); +}; + +MetadataCard.displayName = "MetadataCard"; +export default MetadataCard; diff --git a/src/components/Metadata/fixtures.ts b/src/components/Metadata/fixtures.ts new file mode 100644 index 000000000..970116c99 --- /dev/null +++ b/src/components/Metadata/fixtures.ts @@ -0,0 +1,22 @@ +import { MetadataProps } from "./Metadata"; + +export const props: MetadataProps = { + data: { + metadata: [ + { + key: "key", + value: "value" + }, + { + key: "key2", + value: '{\n "jsonValue": "some-value"\n}' + }, + { + key: "key3", + value: "some-value" + } + ], + privateMetadata: [] + }, + onChange: () => undefined +}; diff --git a/src/components/Metadata/index.ts b/src/components/Metadata/index.ts new file mode 100644 index 000000000..4ded27123 --- /dev/null +++ b/src/components/Metadata/index.ts @@ -0,0 +1,5 @@ +export * from "./Metadata"; +export * from "./MetadataCard"; +export * from "./types"; +export { default } from "./Metadata"; +export { default as MetadataCard } from "./MetadataCard"; diff --git a/src/components/Metadata/styles.ts b/src/components/Metadata/styles.ts new file mode 100644 index 000000000..3d7c506fd --- /dev/null +++ b/src/components/Metadata/styles.ts @@ -0,0 +1,66 @@ +import makeStyles from "@material-ui/core/styles/makeStyles"; + +const useStyles = makeStyles( + theme => { + const colAction: React.CSSProperties = { + textAlign: "right", + width: 130 + }; + const colName: React.CSSProperties = { + width: 220 + }; + + return { + colAction: { + "&:last-child": { + ...colAction, + paddingRight: theme.spacing() + } + }, + colActionHeader: { + ...colAction + }, + colName: { + ...colName, + verticalAlign: "top" + }, + colNameHeader: { + ...colName + }, + colValue: {}, + content: { + paddingBottom: 0, + paddingTop: theme.spacing() + }, + emptyContainer: { + borderBottom: `1px solid ${theme.palette.divider}`, + paddingBottom: theme.spacing(4), + paddingTop: theme.spacing(3), + textAlign: "center" + }, + emptyImage: { + display: "block", + marginBottom: theme.spacing(2) + }, + input: { + padding: theme.spacing(0.5, 2) + }, + nameInput: { + padding: `13px 16px` + }, + table: { + tableLayout: "fixed" + }, + togglable: { + alignItems: "center", + display: "flex", + justifyContent: "space-between" + } + }; + }, + { + name: "Metadata" + } +); + +export default useStyles; diff --git a/src/components/Metadata/types.ts b/src/components/Metadata/types.ts new file mode 100644 index 000000000..9572572b9 --- /dev/null +++ b/src/components/Metadata/types.ts @@ -0,0 +1,21 @@ +import { MetadataInput } from "@saleor/types/globalTypes"; + +export enum EventDataAction { + add = "add", + delete = "delete", + update = "update" +} +export enum EventDataField { + name = "name", + value = "value" +} +export interface EventData { + action: EventDataAction; + field: EventDataField | null; + fieldIndex: number | null; + value: string; +} +export interface MetadataFormData { + metadata: MetadataInput[]; + privateMetadata: MetadataInput[]; +} diff --git a/src/components/Metadata/utils.ts b/src/components/Metadata/utils.ts new file mode 100644 index 000000000..28517b900 --- /dev/null +++ b/src/components/Metadata/utils.ts @@ -0,0 +1,42 @@ +import { ChangeEvent } from "@saleor/hooks/useForm"; + +import { nameSeparator } from "./MetadataCard"; +import { EventData, EventDataAction, EventDataField } from "./types"; + +export function parseEventData(event: ChangeEvent): EventData { + let action: EventDataAction; + let field: EventDataField = null; + let fieldIndex: number = null; + let value: string = null; + + if (event.target.name.includes(EventDataField.name)) { + action = EventDataAction.update; + field = EventDataField.name; + fieldIndex = parseInt(event.target.name.split(nameSeparator)[1], 0); + value = event.target.value; + } + if (event.target.name.includes(EventDataField.value)) { + action = EventDataAction.update; + field = EventDataField.value; + fieldIndex = parseInt(event.target.name.split(nameSeparator)[1], 0); + value = event.target.value; + } + if (event.target.name === EventDataAction.add) { + action = EventDataAction.add; + } + if (event.target.name === EventDataAction.delete) { + action = EventDataAction.delete; + fieldIndex = event.target.value; + } + + return { + action, + field, + fieldIndex, + value + }; +} + +export function getDataKey(isPrivate: boolean) { + return isPrivate ? "privateMetadata" : "metadata"; +} diff --git a/src/fragments/attributes.ts b/src/fragments/attributes.ts index 1cbc7172a..66332cc49 100644 --- a/src/fragments/attributes.ts +++ b/src/fragments/attributes.ts @@ -1,5 +1,7 @@ import gql from "graphql-tag"; +import { metadataFragment } from "./metadata"; + export const attributeFragment = gql` fragment AttributeFragment on Attribute { id @@ -13,8 +15,10 @@ export const attributeFragment = gql` export const attributeDetailsFragment = gql` ${attributeFragment} + ${metadataFragment} fragment AttributeDetailsFragment on Attribute { ...AttributeFragment + ...MetadataFragment availableInGrid inputType storefrontSearchPosition diff --git a/src/fragments/categories.ts b/src/fragments/categories.ts index 9953b0168..3eebe58d9 100644 --- a/src/fragments/categories.ts +++ b/src/fragments/categories.ts @@ -1,5 +1,7 @@ import gql from "graphql-tag"; +import { metadataFragment } from "./metadata"; + export const categoryFragment = gql` fragment CategoryFragment on Category { id @@ -13,8 +15,10 @@ export const categoryFragment = gql` } `; export const categoryDetailsFragment = gql` + ${metadataFragment} fragment CategoryDetailsFragment on Category { id + ...MetadataFragment backgroundImage { alt url diff --git a/src/fragments/collections.ts b/src/fragments/collections.ts index 910f39084..a27129ce3 100644 --- a/src/fragments/collections.ts +++ b/src/fragments/collections.ts @@ -1,5 +1,7 @@ import gql from "graphql-tag"; +import { metadataFragment } from "./metadata"; + export const collectionFragment = gql` fragment CollectionFragment on Collection { id @@ -10,8 +12,10 @@ export const collectionFragment = gql` export const collectionDetailsFragment = gql` ${collectionFragment} + ${metadataFragment} fragment CollectionDetailsFragment on Collection { ...CollectionFragment + ...MetadataFragment backgroundImage { alt url diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index 538e310b5..f4bd5d788 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -134,3 +134,10 @@ export const pluginErrorFragment = gql` field } `; + +export const metadataErrorFragment = gql` + fragment MetadataErrorFragment on MetadataError { + code + field + } +`; diff --git a/src/fragments/metadata.ts b/src/fragments/metadata.ts index ce91f1fdc..2378d8b9e 100644 --- a/src/fragments/metadata.ts +++ b/src/fragments/metadata.ts @@ -5,7 +5,7 @@ export const metadataFragment = gql` key value } - fragment Metadata on ObjectWithMetadata { + fragment MetadataFragment on ObjectWithMetadata { metadata { ...MetadataItem } diff --git a/src/fragments/productTypes.ts b/src/fragments/productTypes.ts index 0e6f00ef7..981dbe571 100644 --- a/src/fragments/productTypes.ts +++ b/src/fragments/productTypes.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; import { attributeFragment } from "./attributes"; +import { metadataFragment } from "./metadata"; export const productTypeFragment = gql` fragment ProductTypeFragment on ProductType { @@ -18,8 +19,10 @@ export const productTypeFragment = gql` export const productTypeDetailsFragment = gql` ${attributeFragment} ${productTypeFragment} + ${metadataFragment} fragment ProductTypeDetailsFragment on ProductType { ...ProductTypeFragment + ...MetadataFragment productAttributes { ...AttributeFragment } diff --git a/src/fragments/products.ts b/src/fragments/products.ts index 234384eba..9400dbaf5 100644 --- a/src/fragments/products.ts +++ b/src/fragments/products.ts @@ -1,5 +1,6 @@ import gql from "graphql-tag"; +import { metadataFragment } from "./metadata"; import { weightFragment } from "./weight"; export const stockFragment = gql` @@ -105,8 +106,10 @@ export const productFragmentDetails = gql` ${productVariantAttributesFragment} ${stockFragment} ${weightFragment} + ${metadataFragment} fragment Product on Product { ...ProductVariantAttributesFragment + ...MetadataFragment name descriptionJson seoTitle @@ -181,8 +184,10 @@ export const fragmentVariant = gql` ${fragmentProductImage} ${stockFragment} ${weightFragment} + ${metadataFragment} fragment ProductVariant on ProductVariant { id + ...MetadataFragment attributes { attribute { id diff --git a/src/fragments/types/AttributeDetailsFragment.ts b/src/fragments/types/AttributeDetailsFragment.ts index 11a22408b..6d9faedd0 100644 --- a/src/fragments/types/AttributeDetailsFragment.ts +++ b/src/fragments/types/AttributeDetailsFragment.ts @@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/global // GraphQL fragment: AttributeDetailsFragment // ==================================================== +export interface AttributeDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AttributeDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AttributeDetailsFragment_values { __typename: "AttributeValue"; id: string; @@ -24,6 +36,8 @@ export interface AttributeDetailsFragment { visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; + metadata: (AttributeDetailsFragment_metadata | null)[]; + privateMetadata: (AttributeDetailsFragment_privateMetadata | null)[]; availableInGrid: boolean; inputType: AttributeInputTypeEnum | null; storefrontSearchPosition: number; diff --git a/src/fragments/types/CategoryDetailsFragment.ts b/src/fragments/types/CategoryDetailsFragment.ts index 111a0e6c2..49e5640ea 100644 --- a/src/fragments/types/CategoryDetailsFragment.ts +++ b/src/fragments/types/CategoryDetailsFragment.ts @@ -6,6 +6,18 @@ // GraphQL fragment: CategoryDetailsFragment // ==================================================== +export interface CategoryDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CategoryDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CategoryDetailsFragment_backgroundImage { __typename: "Image"; alt: string | null; @@ -20,6 +32,8 @@ export interface CategoryDetailsFragment_parent { export interface CategoryDetailsFragment { __typename: "Category"; id: string; + metadata: (CategoryDetailsFragment_metadata | null)[]; + privateMetadata: (CategoryDetailsFragment_privateMetadata | null)[]; backgroundImage: CategoryDetailsFragment_backgroundImage | null; name: string; descriptionJson: any; diff --git a/src/fragments/types/CollectionDetailsFragment.ts b/src/fragments/types/CollectionDetailsFragment.ts index 24ae24d14..d2dc154a8 100644 --- a/src/fragments/types/CollectionDetailsFragment.ts +++ b/src/fragments/types/CollectionDetailsFragment.ts @@ -6,6 +6,18 @@ // GraphQL fragment: CollectionDetailsFragment // ==================================================== +export interface CollectionDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface CollectionDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface CollectionDetailsFragment_backgroundImage { __typename: "Image"; alt: string | null; @@ -17,6 +29,8 @@ export interface CollectionDetailsFragment { id: string; isPublished: boolean; name: string; + metadata: (CollectionDetailsFragment_metadata | null)[]; + privateMetadata: (CollectionDetailsFragment_privateMetadata | null)[]; backgroundImage: CollectionDetailsFragment_backgroundImage | null; descriptionJson: any; publicationDate: any | null; diff --git a/src/fragments/types/MetadataErrorFragment.ts b/src/fragments/types/MetadataErrorFragment.ts new file mode 100644 index 000000000..7c669c123 --- /dev/null +++ b/src/fragments/types/MetadataErrorFragment.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { MetadataErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: MetadataErrorFragment +// ==================================================== + +export interface MetadataErrorFragment { + __typename: "MetadataError"; + code: MetadataErrorCode; + field: string | null; +} diff --git a/src/fragments/types/MetadataFragment.ts b/src/fragments/types/MetadataFragment.ts new file mode 100644 index 000000000..4c1b70245 --- /dev/null +++ b/src/fragments/types/MetadataFragment.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: MetadataFragment +// ==================================================== + +export interface MetadataFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface MetadataFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface MetadataFragment { + __typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice"; + metadata: (MetadataFragment_metadata | null)[]; + privateMetadata: (MetadataFragment_privateMetadata | null)[]; +} diff --git a/src/fragments/types/Product.ts b/src/fragments/types/Product.ts index dc8c1e5b0..26f229764 100644 --- a/src/fragments/types/Product.ts +++ b/src/fragments/types/Product.ts @@ -93,6 +93,18 @@ export interface Product_pricing { priceRangeUndiscounted: Product_pricing_priceRangeUndiscounted | null; } +export interface Product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface Product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface Product_category { __typename: "Category"; id: string; @@ -180,6 +192,8 @@ export interface Product { attributes: Product_attributes[]; productType: Product_productType; pricing: Product_pricing | null; + metadata: (Product_metadata | null)[]; + privateMetadata: (Product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/fragments/types/ProductTypeDetailsFragment.ts b/src/fragments/types/ProductTypeDetailsFragment.ts index ecefce57b..3c9851cd3 100644 --- a/src/fragments/types/ProductTypeDetailsFragment.ts +++ b/src/fragments/types/ProductTypeDetailsFragment.ts @@ -14,6 +14,18 @@ export interface ProductTypeDetailsFragment_taxType { taxCode: string | null; } +export interface ProductTypeDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductTypeDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductTypeDetailsFragment_productAttributes { __typename: "Attribute"; id: string; @@ -47,6 +59,8 @@ export interface ProductTypeDetailsFragment { hasVariants: boolean; isShippingRequired: boolean; taxType: ProductTypeDetailsFragment_taxType | null; + metadata: (ProductTypeDetailsFragment_metadata | null)[]; + privateMetadata: (ProductTypeDetailsFragment_privateMetadata | null)[]; productAttributes: (ProductTypeDetailsFragment_productAttributes | null)[] | null; variantAttributes: (ProductTypeDetailsFragment_variantAttributes | null)[] | null; weight: ProductTypeDetailsFragment_weight | null; diff --git a/src/fragments/types/ProductVariant.ts b/src/fragments/types/ProductVariant.ts index b2295f03c..0f7fcf4fc 100644 --- a/src/fragments/types/ProductVariant.ts +++ b/src/fragments/types/ProductVariant.ts @@ -8,6 +8,18 @@ import { WeightUnitsEnum } from "./../../types/globalTypes"; // GraphQL fragment: ProductVariant // ==================================================== +export interface ProductVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -114,6 +126,8 @@ export interface ProductVariant_weight { export interface ProductVariant { __typename: "ProductVariant"; id: string; + metadata: (ProductVariant_metadata | null)[]; + privateMetadata: (ProductVariant_privateMetadata | null)[]; attributes: ProductVariant_attributes[]; costPrice: ProductVariant_costPrice | null; images: (ProductVariant_images | null)[] | null; diff --git a/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx b/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx index 469695f1e..a4a5391df 100644 --- a/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx +++ b/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx @@ -5,6 +5,8 @@ import Container from "@saleor/components/Container"; import ControlledSwitch from "@saleor/components/ControlledSwitch"; import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; +import Metadata from "@saleor/components/Metadata/Metadata"; +import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; @@ -13,6 +15,8 @@ import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { ListActions, ReorderEvent, UserError } from "@saleor/types"; import { AttributeTypeEnum, WeightUnitsEnum } from "@saleor/types/globalTypes"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import React from "react"; import { useIntl } from "react-intl"; @@ -30,7 +34,7 @@ interface ChoiceType { value: string; } -export interface ProductTypeForm { +export interface ProductTypeForm extends MetadataFormData { name: string; hasVariants: boolean; isShippingRequired: boolean; @@ -92,6 +96,12 @@ const ProductTypeDetailsPage: React.FC = ({ onSubmit }) => { const intl = useIntl(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const [taxTypeDisplayName, setTaxTypeDisplayName] = useStateFromProps( maybe(() => productType.taxType.description, "") ); @@ -104,7 +114,9 @@ const ProductTypeDetailsPage: React.FC = ({ maybe(() => productType.isShippingRequired) !== undefined ? productType.isShippingRequired : false, + metadata: productType?.metadata?.map(mapMetadataItemToInput), name: maybe(() => productType.name) !== undefined ? productType.name : "", + privateMetadata: productType?.privateMetadata?.map(mapMetadataItemToInput), productAttributes: maybe(() => productType.productAttributes) !== undefined ? productType.productAttributes.map(attribute => ({ @@ -122,97 +134,117 @@ const ProductTypeDetailsPage: React.FC = ({ : [], weight: maybe(() => productType.weight.value) }; + + const handleSubmit = (data: ProductTypeForm) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + + onSubmit({ + ...data, + metadata, + privateMetadata + }); + }; + return ( -
- {({ change, data, hasChanged, submit }) => ( - - - {intl.formatMessage(sectionNames.productTypes)} - - - -
- - - - handleTaxTypeChange( - event, - taxTypes, - change, - setTaxTypeDisplayName - ) - } - /> - - productType.productAttributes)} - disabled={disabled} - type={AttributeTypeEnum.PRODUCT} - onAttributeAssign={onAttributeAdd} - onAttributeClick={onAttributeClick} - onAttributeReorder={(event: ReorderEvent) => - onAttributeReorder(event, AttributeTypeEnum.PRODUCT) - } - onAttributeUnassign={onAttributeUnassign} - {...productAttributeList} - /> - - onHasVariantsToggle(event.target.value)} - /> - {data.hasVariants && ( - <> - - productType.variantAttributes)} - disabled={disabled} - type={AttributeTypeEnum.VARIANT} - onAttributeAssign={onAttributeAdd} - onAttributeClick={onAttributeClick} - onAttributeReorder={(event: ReorderEvent) => - onAttributeReorder(event, AttributeTypeEnum.VARIANT) - } - onAttributeUnassign={onAttributeUnassign} - {...variantAttributeList} - /> - - )} -
-
- -
-
- -
- )} + + {({ change, data, hasChanged, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.productTypes)} + + + +
+ + + + handleTaxTypeChange( + event, + taxTypes, + change, + setTaxTypeDisplayName + ) + } + /> + + productType.productAttributes)} + disabled={disabled} + type={AttributeTypeEnum.PRODUCT} + onAttributeAssign={onAttributeAdd} + onAttributeClick={onAttributeClick} + onAttributeReorder={(event: ReorderEvent) => + onAttributeReorder(event, AttributeTypeEnum.PRODUCT) + } + onAttributeUnassign={onAttributeUnassign} + {...productAttributeList} + /> + + onHasVariantsToggle(event.target.value)} + /> + {data.hasVariants && ( + <> + + productType.variantAttributes)} + disabled={disabled} + type={AttributeTypeEnum.VARIANT} + onAttributeAssign={onAttributeAdd} + onAttributeClick={onAttributeClick} + onAttributeReorder={(event: ReorderEvent) => + onAttributeReorder(event, AttributeTypeEnum.VARIANT) + } + onAttributeUnassign={onAttributeUnassign} + {...variantAttributeList} + /> + + )} + + +
+
+ +
+
+ +
+ ); + }}
); }; diff --git a/src/productTypes/containers/ProductTypeOperations.tsx b/src/productTypes/containers/ProductTypeOperations.tsx index 15798e127..b50847d4f 100644 --- a/src/productTypes/containers/ProductTypeOperations.tsx +++ b/src/productTypes/containers/ProductTypeOperations.tsx @@ -12,7 +12,6 @@ import { ProductTypeAttributeReorderMutation, TypedAssignAttributeMutation, TypedProductTypeDeleteMutation, - TypedProductTypeUpdateMutation, TypedUnassignAttributeMutation } from "../mutations"; import { @@ -27,10 +26,6 @@ import { ProductTypeDelete, ProductTypeDeleteVariables } from "../types/ProductTypeDelete"; -import { - ProductTypeUpdate, - ProductTypeUpdateVariables -} from "../types/ProductTypeUpdate"; import { UnassignAttribute, UnassignAttributeVariables @@ -75,17 +70,12 @@ interface ProductTypeOperationsProps { ProductTypeAttributeReorder, ProductTypeAttributeReorderVariables >; - updateProductType: PartialMutationProviderOutput< - ProductTypeUpdate, - ProductTypeUpdateVariables - >; }) => React.ReactNode; productType: ProductTypeDetailsFragment; onAssignAttribute: (data: AssignAttribute) => void; onUnassignAttribute: (data: UnassignAttribute) => void; onProductTypeAttributeReorder: (data: ProductTypeAttributeReorder) => void; onProductTypeDelete: (data: ProductTypeDelete) => void; - onProductTypeUpdate: (data: ProductTypeUpdate) => void; } const ProductTypeOperations: React.FC = ({ @@ -94,85 +84,72 @@ const ProductTypeOperations: React.FC = ({ onAssignAttribute, onUnassignAttribute, onProductTypeAttributeReorder, - onProductTypeDelete, - onProductTypeUpdate + onProductTypeDelete }) => ( {(...deleteProductType) => ( - - {(...updateProductType) => ( - - {(...assignAttribute) => ( - - {(...unassignAttribute) => ( - - {( - reorderAttributeMutation, - reorderAttributeMutationResult - ) => { - const reorderAttributeMutationFn: MutationFunction< - ProductTypeAttributeReorder, - ProductTypeAttributeReorderVariables - > = opts => { - const optimisticResponse: ProductTypeAttributeReorder = { - productTypeReorderAttributes: { - __typename: "ProductTypeReorderAttributes" as "ProductTypeReorderAttributes", - errors: [], - productType: { - ...productType, - productAttributes: - opts.variables.type === - AttributeTypeEnum.PRODUCT - ? moveAttribute( - productType.productAttributes, - opts.variables.move - ) - : productType.productAttributes, - variantAttributes: - opts.variables.type === - AttributeTypeEnum.VARIANT - ? moveAttribute( - productType.variantAttributes, - opts.variables.move - ) - : productType.variantAttributes - } - } - }; - return reorderAttributeMutation({ - ...opts, - optimisticResponse - }); - }; + + {(...assignAttribute) => ( + + {(...unassignAttribute) => ( + + {(reorderAttributeMutation, reorderAttributeMutationResult) => { + const reorderAttributeMutationFn: MutationFunction< + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables + > = opts => { + const optimisticResponse: ProductTypeAttributeReorder = { + productTypeReorderAttributes: { + __typename: "ProductTypeReorderAttributes" as "ProductTypeReorderAttributes", + errors: [], + productType: { + ...productType, + productAttributes: + opts.variables.type === AttributeTypeEnum.PRODUCT + ? moveAttribute( + productType.productAttributes, + opts.variables.move + ) + : productType.productAttributes, + variantAttributes: + opts.variables.type === AttributeTypeEnum.VARIANT + ? moveAttribute( + productType.variantAttributes, + opts.variables.move + ) + : productType.variantAttributes + } + } + }; + return reorderAttributeMutation({ + ...opts, + optimisticResponse + }); + }; - return children({ - assignAttribute: getMutationProviderData( - ...assignAttribute - ), - deleteProductType: getMutationProviderData( - ...deleteProductType - ), - reorderAttribute: getMutationProviderData( - reorderAttributeMutationFn, - reorderAttributeMutationResult - ), - unassignAttribute: getMutationProviderData( - ...unassignAttribute - ), - updateProductType: getMutationProviderData( - ...updateProductType - ) - }); - }} - - )} - + return children({ + assignAttribute: getMutationProviderData( + ...assignAttribute + ), + deleteProductType: getMutationProviderData( + ...deleteProductType + ), + reorderAttribute: getMutationProviderData( + reorderAttributeMutationFn, + reorderAttributeMutationResult + ), + unassignAttribute: getMutationProviderData( + ...unassignAttribute + ) + }); + }} + )} - + )} - + )} ); diff --git a/src/productTypes/fixtures.ts b/src/productTypes/fixtures.ts index 50d1ff5fe..f8400876c 100644 --- a/src/productTypes/fixtures.ts +++ b/src/productTypes/fixtures.ts @@ -550,7 +550,15 @@ export const productType: ProductTypeDetails_productType = { hasVariants: false, id: "UHJvZHVjdFR5cGU6NQ==", isShippingRequired: false, + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], name: "E-books", + privateMetadata: [], productAttributes: [ { __typename: "Attribute" as "Attribute", diff --git a/src/productTypes/mutations.ts b/src/productTypes/mutations.ts index b7c9fcc28..bd73b86b3 100644 --- a/src/productTypes/mutations.ts +++ b/src/productTypes/mutations.ts @@ -1,4 +1,5 @@ import { productTypeDetailsFragment } from "@saleor/fragments/productTypes"; +import makeMutation from "@saleor/hooks/makeMutation"; import gql from "graphql-tag"; import { TypedMutation } from "../mutations"; @@ -78,7 +79,7 @@ export const productTypeUpdateMutation = gql` } } `; -export const TypedProductTypeUpdateMutation = TypedMutation< +export const useProductTypeUpdateMutation = makeMutation< ProductTypeUpdate, ProductTypeUpdateVariables >(productTypeUpdateMutation); diff --git a/src/productTypes/types/AssignAttribute.ts b/src/productTypes/types/AssignAttribute.ts index 580c40eb3..365276b7a 100644 --- a/src/productTypes/types/AssignAttribute.ts +++ b/src/productTypes/types/AssignAttribute.ts @@ -20,6 +20,18 @@ export interface AssignAttribute_attributeAssign_productType_taxType { taxCode: string | null; } +export interface AssignAttribute_attributeAssign_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AssignAttribute_attributeAssign_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface AssignAttribute_attributeAssign_productType_productAttributes { __typename: "Attribute"; id: string; @@ -53,6 +65,8 @@ export interface AssignAttribute_attributeAssign_productType { hasVariants: boolean; isShippingRequired: boolean; taxType: AssignAttribute_attributeAssign_productType_taxType | null; + metadata: (AssignAttribute_attributeAssign_productType_metadata | null)[]; + privateMetadata: (AssignAttribute_attributeAssign_productType_privateMetadata | null)[]; productAttributes: (AssignAttribute_attributeAssign_productType_productAttributes | null)[] | null; variantAttributes: (AssignAttribute_attributeAssign_productType_variantAttributes | null)[] | null; weight: AssignAttribute_attributeAssign_productType_weight | null; diff --git a/src/productTypes/types/ProductTypeAttributeReorder.ts b/src/productTypes/types/ProductTypeAttributeReorder.ts index e7153d47b..bafaa3ca6 100644 --- a/src/productTypes/types/ProductTypeAttributeReorder.ts +++ b/src/productTypes/types/ProductTypeAttributeReorder.ts @@ -20,6 +20,18 @@ export interface ProductTypeAttributeReorder_productTypeReorderAttributes_produc taxCode: string | null; } +export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes { __typename: "Attribute"; id: string; @@ -53,6 +65,8 @@ export interface ProductTypeAttributeReorder_productTypeReorderAttributes_produc hasVariants: boolean; isShippingRequired: boolean; taxType: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_taxType | null; + metadata: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_metadata | null)[]; + privateMetadata: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_privateMetadata | null)[]; productAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_variantAttributes | null)[] | null; weight: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_weight | null; diff --git a/src/productTypes/types/ProductTypeCreate.ts b/src/productTypes/types/ProductTypeCreate.ts index f980a1087..559209242 100644 --- a/src/productTypes/types/ProductTypeCreate.ts +++ b/src/productTypes/types/ProductTypeCreate.ts @@ -20,6 +20,18 @@ export interface ProductTypeCreate_productTypeCreate_productType_taxType { taxCode: string | null; } +export interface ProductTypeCreate_productTypeCreate_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductTypeCreate_productTypeCreate_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductTypeCreate_productTypeCreate_productType_productAttributes { __typename: "Attribute"; id: string; @@ -53,6 +65,8 @@ export interface ProductTypeCreate_productTypeCreate_productType { hasVariants: boolean; isShippingRequired: boolean; taxType: ProductTypeCreate_productTypeCreate_productType_taxType | null; + metadata: (ProductTypeCreate_productTypeCreate_productType_metadata | null)[]; + privateMetadata: (ProductTypeCreate_productTypeCreate_productType_privateMetadata | null)[]; productAttributes: (ProductTypeCreate_productTypeCreate_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeCreate_productTypeCreate_productType_variantAttributes | null)[] | null; weight: ProductTypeCreate_productTypeCreate_productType_weight | null; diff --git a/src/productTypes/types/ProductTypeDetails.ts b/src/productTypes/types/ProductTypeDetails.ts index 71ad2aa4f..fcfb86707 100644 --- a/src/productTypes/types/ProductTypeDetails.ts +++ b/src/productTypes/types/ProductTypeDetails.ts @@ -14,6 +14,18 @@ export interface ProductTypeDetails_productType_taxType { taxCode: string | null; } +export interface ProductTypeDetails_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductTypeDetails_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductTypeDetails_productType_productAttributes { __typename: "Attribute"; id: string; @@ -47,6 +59,8 @@ export interface ProductTypeDetails_productType { hasVariants: boolean; isShippingRequired: boolean; taxType: ProductTypeDetails_productType_taxType | null; + metadata: (ProductTypeDetails_productType_metadata | null)[]; + privateMetadata: (ProductTypeDetails_productType_privateMetadata | null)[]; productAttributes: (ProductTypeDetails_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeDetails_productType_variantAttributes | null)[] | null; weight: ProductTypeDetails_productType_weight | null; diff --git a/src/productTypes/types/ProductTypeUpdate.ts b/src/productTypes/types/ProductTypeUpdate.ts index 566314d02..c2e43a3fd 100644 --- a/src/productTypes/types/ProductTypeUpdate.ts +++ b/src/productTypes/types/ProductTypeUpdate.ts @@ -20,6 +20,18 @@ export interface ProductTypeUpdate_productTypeUpdate_productType_taxType { taxCode: string | null; } +export interface ProductTypeUpdate_productTypeUpdate_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductTypeUpdate_productTypeUpdate_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductTypeUpdate_productTypeUpdate_productType_productAttributes { __typename: "Attribute"; id: string; @@ -53,6 +65,8 @@ export interface ProductTypeUpdate_productTypeUpdate_productType { hasVariants: boolean; isShippingRequired: boolean; taxType: ProductTypeUpdate_productTypeUpdate_productType_taxType | null; + metadata: (ProductTypeUpdate_productTypeUpdate_productType_metadata | null)[]; + privateMetadata: (ProductTypeUpdate_productTypeUpdate_productType_privateMetadata | null)[]; productAttributes: (ProductTypeUpdate_productTypeUpdate_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeUpdate_productTypeUpdate_productType_variantAttributes | null)[] | null; weight: ProductTypeUpdate_productTypeUpdate_productType_weight | null; diff --git a/src/productTypes/types/UnassignAttribute.ts b/src/productTypes/types/UnassignAttribute.ts index 056ecd287..555b11ade 100644 --- a/src/productTypes/types/UnassignAttribute.ts +++ b/src/productTypes/types/UnassignAttribute.ts @@ -20,6 +20,18 @@ export interface UnassignAttribute_attributeUnassign_productType_taxType { taxCode: string | null; } +export interface UnassignAttribute_attributeUnassign_productType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface UnassignAttribute_attributeUnassign_productType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface UnassignAttribute_attributeUnassign_productType_productAttributes { __typename: "Attribute"; id: string; @@ -53,6 +65,8 @@ export interface UnassignAttribute_attributeUnassign_productType { hasVariants: boolean; isShippingRequired: boolean; taxType: UnassignAttribute_attributeUnassign_productType_taxType | null; + metadata: (UnassignAttribute_attributeUnassign_productType_metadata | null)[]; + privateMetadata: (UnassignAttribute_attributeUnassign_productType_privateMetadata | null)[]; productAttributes: (UnassignAttribute_attributeUnassign_productType_productAttributes | null)[] | null; variantAttributes: (UnassignAttribute_attributeUnassign_productType_variantAttributes | null)[] | null; weight: UnassignAttribute_attributeUnassign_productType_weight | null; diff --git a/src/productTypes/views/ProductTypeUpdate/errors.tsx b/src/productTypes/views/ProductTypeUpdate/errors.tsx deleted file mode 100644 index 5e2c83535..000000000 --- a/src/productTypes/views/ProductTypeUpdate/errors.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; - -import { UserError } from "../../../types"; - -interface ProductTypeUpdateErrorsState { - addAttributeErrors: UserError[]; - editAttributeErrors: UserError[]; - formErrors: UserError[]; -} -interface ProductTypeUpdateErrorsProps { - children: (props: { - errors: ProductTypeUpdateErrorsState; - set: { - addAttributeErrors: (errors: UserError[]) => void; - editAttributeErrors: (errors: UserError[]) => void; - formErrors: (errors: UserError[]) => void; - }; - }) => React.ReactNode; -} - -export class ProductTypeUpdateErrors extends React.Component< - ProductTypeUpdateErrorsProps, - ProductTypeUpdateErrorsState -> { - state: ProductTypeUpdateErrorsState = { - addAttributeErrors: [], - editAttributeErrors: [], - formErrors: [] - }; - - render() { - return this.props.children({ - errors: this.state, - set: { - addAttributeErrors: (addAttributeErrors: UserError[]) => - this.setState({ addAttributeErrors }), - editAttributeErrors: (editAttributeErrors: UserError[]) => - this.setState({ editAttributeErrors }), - formErrors: (formErrors: UserError[]) => this.setState({ formErrors }) - } - }); - } -} -export default ProductTypeUpdateErrors; diff --git a/src/productTypes/views/ProductTypeUpdate/index.tsx b/src/productTypes/views/ProductTypeUpdate/index.tsx index 9c7e57646..eb987853f 100644 --- a/src/productTypes/views/ProductTypeUpdate/index.tsx +++ b/src/productTypes/views/ProductTypeUpdate/index.tsx @@ -9,8 +9,14 @@ import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog"; +import { useProductTypeUpdateMutation } from "@saleor/productTypes/mutations"; import { ReorderEvent } from "@saleor/types"; import { AttributeTypeEnum } from "@saleor/types/globalTypes"; +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"; @@ -25,14 +31,12 @@ import useAvailableAttributeSearch from "../../hooks/useAvailableAttributeSearch import { TypedProductTypeDetailsQuery } from "../../queries"; import { AssignAttribute } from "../../types/AssignAttribute"; import { ProductTypeDelete } from "../../types/ProductTypeDelete"; -import { ProductTypeUpdate as ProductTypeUpdateMutation } from "../../types/ProductTypeUpdate"; import { UnassignAttribute } from "../../types/UnassignAttribute"; import { productTypeListUrl, productTypeUrl, ProductTypeUrlQueryParams } from "../../urls"; -import { ProductTypeUpdateErrors } from "./errors"; interface ProductTypeUpdateProps { id: string; @@ -54,361 +58,367 @@ export const ProductTypeUpdate: React.FC = ({ id } }); + const [errors, setErrors] = React.useState({ + addAttributeErrors: [], + editAttributeErrors: [], + formErrors: [] + }); + + const [ + updateProductType, + updateProductTypeOpts + ] = useProductTypeUpdateMutation({ + onCompleted: updateData => { + if ( + !updateData.productTypeUpdate.errors || + updateData.productTypeUpdate.errors.length === 0 + ) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + } else if ( + updateData.productTypeUpdate.errors !== null && + updateData.productTypeUpdate.errors.length > 0 + ) { + setErrors(prevErrors => ({ + ...prevErrors, + formErrors: updateData.productTypeUpdate.errors + })); + } + } + }); + + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const handleBack = () => navigate(productTypeListUrl()); + const handleProductTypeUpdate = async (formData: ProductTypeForm) => { + const result = await updateProductType({ + variables: { + id, + input: { + hasVariants: formData.hasVariants, + isShippingRequired: formData.isShippingRequired, + name: formData.name, + productAttributes: formData.productAttributes.map( + choice => choice.value + ), + taxCode: formData.taxType, + variantAttributes: formData.variantAttributes.map( + choice => choice.value + ), + weight: formData.weight + } + } + }); + + return result.data.productTypeUpdate.errors; + }; + return ( - - {({ errors, set: setErrors }) => ( - - {({ data, loading: dataLoading }) => { - const productType = data?.productType; + + {({ data, loading: dataLoading }) => { + const productType = data?.productType; - if (productType === null) { - return ; - } + if (productType === null) { + return ; + } - const closeModal = () => navigate(productTypeUrl(id), true); + const closeModal = () => navigate(productTypeUrl(id), true); - const handleAttributeAssignSuccess = (data: AssignAttribute) => { - if (data.attributeAssign.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) + const handleAttributeAssignSuccess = (data: AssignAttribute) => { + if (data.attributeAssign.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + } else if ( + data.attributeAssign.errors !== null && + data.attributeAssign.errors.length > 0 + ) { + setErrors(prevErrors => ({ + ...prevErrors, + addAttributeErrors: data.attributeAssign.errors + })); + } + }; + const handleAttributeUnassignSuccess = (data: UnassignAttribute) => { + if (data.attributeUnassign.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + productAttributeListActions.reset(); + variantAttributeListActions.reset(); + } + }; + const handleProductTypeDeleteSuccess = ( + deleteData: ProductTypeDelete + ) => { + if (deleteData.productTypeDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Product type deleted" + }) + }); + navigate(productTypeListUrl(), true); + } + }; + const handleSubmit = createMetadataUpdateHandler( + data?.productType, + handleProductTypeUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + + return ( + data.productType)} + onAssignAttribute={handleAttributeAssignSuccess} + onUnassignAttribute={handleAttributeUnassignSuccess} + onProductTypeDelete={handleProductTypeDeleteSuccess} + onProductTypeAttributeReorder={() => undefined} + > + {({ + assignAttribute, + deleteProductType, + unassignAttribute, + reorderAttribute + }) => { + const handleProductTypeDelete = () => + deleteProductType.mutate({ id }); + const handleProductTypeVariantsToggle = (hasVariants: boolean) => + updateProductType({ + variables: { + id, + input: { + hasVariants + } + } }); - closeModal(); - } else if ( - data.attributeAssign.errors !== null && - data.attributeAssign.errors.length > 0 - ) { - setErrors.addAttributeErrors(data.attributeAssign.errors); - } - }; - const handleAttributeUnassignSuccess = ( - data: UnassignAttribute - ) => { - if (data.attributeUnassign.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) + const handleAssignAttribute = () => + assignAttribute.mutate({ + id, + operations: params.ids.map(id => ({ + id, + type: AttributeTypeEnum[params.type] + })) }); - closeModal(); - productAttributeListActions.reset(); - variantAttributeListActions.reset(); - } - }; - const handleProductTypeDeleteSuccess = ( - deleteData: ProductTypeDelete - ) => { - if (deleteData.productTypeDelete.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage({ - defaultMessage: "Product type deleted" - }) + + const handleAttributeUnassign = () => + unassignAttribute.mutate({ + id, + ids: [params.id] }); - navigate(productTypeListUrl(), true); - } - }; - const handleProductTypeUpdateSuccess = ( - updateData: ProductTypeUpdateMutation - ) => { - if ( - !updateData.productTypeUpdate.errors || - updateData.productTypeUpdate.errors.length === 0 - ) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) + + const handleBulkAttributeUnassign = () => + unassignAttribute.mutate({ + id, + ids: params.ids }); - } else if ( - updateData.productTypeUpdate.errors !== null && - updateData.productTypeUpdate.errors.length > 0 - ) { - setErrors.formErrors(updateData.productTypeUpdate.errors); - } - }; - return ( - data.productType)} - onAssignAttribute={handleAttributeAssignSuccess} - onUnassignAttribute={handleAttributeUnassignSuccess} - onProductTypeDelete={handleProductTypeDeleteSuccess} - onProductTypeUpdate={handleProductTypeUpdateSuccess} - onProductTypeAttributeReorder={() => undefined} - > - {({ - assignAttribute, - deleteProductType, - unassignAttribute, - updateProductType, - reorderAttribute - }) => { - const handleProductTypeDelete = () => - deleteProductType.mutate({ id }); - const handleProductTypeUpdate = ( - formData: ProductTypeForm - ) => { - updateProductType.mutate({ - id, - input: { - hasVariants: formData.hasVariants, - isShippingRequired: formData.isShippingRequired, - name: formData.name, - productAttributes: formData.productAttributes.map( - choice => choice.value - ), - taxCode: formData.taxType, - variantAttributes: formData.variantAttributes.map( - choice => choice.value - ), - weight: formData.weight - } - }); - }; - const handleProductTypeVariantsToggle = ( - hasVariants: boolean - ) => - updateProductType.mutate({ - id, - input: { - hasVariants - } - }); - const handleAssignAttribute = () => - assignAttribute.mutate({ - id, - operations: params.ids.map(id => ({ - id, - type: AttributeTypeEnum[params.type] - })) - }); + const loading = updateProductTypeOpts.loading || dataLoading; - const handleAttributeUnassign = () => - unassignAttribute.mutate({ - id, - ids: [params.id] - }); + const handleAttributeReorder = ( + event: ReorderEvent, + type: AttributeTypeEnum + ) => { + const attributes = + type === AttributeTypeEnum.PRODUCT + ? data.productType.productAttributes + : data.productType.variantAttributes; - const handleBulkAttributeUnassign = () => - unassignAttribute.mutate({ - id, - ids: params.ids - }); + reorderAttribute.mutate({ + move: { + id: attributes[event.oldIndex].id, + sortOrder: event.newIndex - event.oldIndex + }, + productTypeId: id, + type + }); + }; - const loading = updateProductType.opts.loading || dataLoading; - - const handleAttributeReorder = ( - event: ReorderEvent, - type: AttributeTypeEnum - ) => { - const attributes = - type === AttributeTypeEnum.PRODUCT - ? data.productType.productAttributes - : data.productType.variantAttributes; - - reorderAttribute.mutate({ - move: { - id: attributes[event.oldIndex].id, - sortOrder: event.newIndex - event.oldIndex - }, - productTypeId: id, - type - }); - }; - - return ( - <> - data.productType.name)} /> - data.shop.defaultWeightUnit - )} - disabled={loading} - errors={errors.formErrors} - pageTitle={maybe(() => data.productType.name)} - productType={maybe(() => data.productType)} - saveButtonBarState={updateProductType.opts.status} - taxTypes={maybe(() => data.taxTypes, [])} - onAttributeAdd={type => - navigate( - productTypeUrl(id, { - action: "assign-attribute", - type - }) - ) - } - onAttributeClick={attributeId => - navigate(attributeUrl(attributeId)) - } - onAttributeReorder={handleAttributeReorder} - onAttributeUnassign={attributeId => - navigate( - productTypeUrl(id, { - action: "unassign-attribute", - id: attributeId - }) - ) - } - onBack={handleBack} - onDelete={() => - navigate( - productTypeUrl(id, { - action: "remove" - }) - ) - } - onHasVariantsToggle={handleProductTypeVariantsToggle} - onSubmit={handleProductTypeUpdate} - productAttributeList={{ - isChecked: productAttributeListActions.isSelected, - selected: - productAttributeListActions.listElements.length, - toggle: productAttributeListActions.toggle, - toggleAll: productAttributeListActions.toggleAll, - toolbar: ( - - ) - }} - variantAttributeList={{ - isChecked: variantAttributeListActions.isSelected, - selected: - variantAttributeListActions.listElements.length, - toggle: variantAttributeListActions.toggle, - toggleAll: variantAttributeListActions.toggleAll, - toolbar: ( - - ) - }} - /> - {!dataLoading && - Object.keys(AttributeTypeEnum).map(key => ( - - result.data.productType.availableAttributes.edges.map( - edge => edge.node - ) - )} - confirmButtonState={assignAttribute.opts.status} - errors={maybe( - () => - assignAttribute.opts.data.attributeAssign.errors.map( - err => err.message - ), - [] - )} - loading={result.loading} - onClose={closeModal} - onSubmit={handleAssignAttribute} - onFetch={search} - onFetchMore={loadMore} - onOpen={result.refetch} - hasMore={maybe( - () => - result.data.productType.availableAttributes - .pageInfo.hasNextPage, - false - )} - open={ - params.action === "assign-attribute" && - params.type === AttributeTypeEnum[key] - } - selected={maybe(() => params.ids, [])} - onToggle={attributeId => { - const ids = maybe(() => params.ids, []); - navigate( - productTypeUrl(id, { - ...params, - ids: ids.includes(attributeId) - ? params.ids.filter( - selectedId => selectedId !== attributeId - ) - : [...ids, attributeId] - }) - ); - }} - key={key} + return ( + <> + data.productType.name)} /> + data.shop.defaultWeightUnit)} + disabled={loading} + errors={errors.formErrors} + pageTitle={maybe(() => data.productType.name)} + productType={maybe(() => data.productType)} + saveButtonBarState={updateProductTypeOpts.status} + taxTypes={maybe(() => data.taxTypes, [])} + onAttributeAdd={type => + navigate( + productTypeUrl(id, { + action: "assign-attribute", + type + }) + ) + } + onAttributeClick={attributeId => + navigate(attributeUrl(attributeId)) + } + onAttributeReorder={handleAttributeReorder} + onAttributeUnassign={attributeId => + navigate( + productTypeUrl(id, { + action: "unassign-attribute", + id: attributeId + }) + ) + } + onBack={handleBack} + onDelete={() => + navigate( + productTypeUrl(id, { + action: "remove" + }) + ) + } + onHasVariantsToggle={handleProductTypeVariantsToggle} + onSubmit={handleSubmit} + productAttributeList={{ + isChecked: productAttributeListActions.isSelected, + selected: productAttributeListActions.listElements.length, + toggle: productAttributeListActions.toggle, + toggleAll: productAttributeListActions.toggleAll, + toolbar: ( + + ) + }} + variantAttributeList={{ + isChecked: variantAttributeListActions.isSelected, + selected: variantAttributeListActions.listElements.length, + toggle: variantAttributeListActions.toggle, + toggleAll: variantAttributeListActions.toggleAll, + toolbar: ( + + ) + }} + /> + {!dataLoading && + Object.keys(AttributeTypeEnum).map(key => ( + + result.data.productType.availableAttributes.edges.map( + edge => edge.node + ) )} - /> - - [ - ...data.productType.productAttributes, - ...data.productType.variantAttributes - ].find(attribute => attribute.id === params.id) - .name, - "..." + assignAttribute.opts.data.attributeAssign.errors.map( + err => err.message + ), + [] )} - confirmButtonState={unassignAttribute.opts.status} + loading={result.loading} onClose={closeModal} - onConfirm={handleAttributeUnassign} - open={params.action === "unassign-attribute"} - productTypeName={maybe( - () => data.productType.name, - "..." + onSubmit={handleAssignAttribute} + onFetch={search} + onFetchMore={loadMore} + onOpen={result.refetch} + hasMore={maybe( + () => + result.data.productType.availableAttributes.pageInfo + .hasNextPage, + false )} + open={ + params.action === "assign-attribute" && + params.type === AttributeTypeEnum[key] + } + selected={maybe(() => params.ids, [])} + onToggle={attributeId => { + const ids = maybe(() => params.ids, []); + navigate( + productTypeUrl(id, { + ...params, + ids: ids.includes(attributeId) + ? params.ids.filter( + selectedId => selectedId !== attributeId + ) + : [...ids, attributeId] + }) + ); + }} + key={key} /> - - ); - }} - - ); - }} - - )} - + ))} + data.productType.name, "...")} + open={params.action === "remove"} + onClose={() => navigate(productTypeUrl(id))} + onConfirm={handleProductTypeDelete} + /> + params.ids.length)} + confirmButtonState={unassignAttribute.opts.status} + onClose={closeModal} + onConfirm={handleBulkAttributeUnassign} + open={params.action === "unassign-attributes"} + productTypeName={maybe(() => data.productType.name, "...")} + /> + + [ + ...data.productType.productAttributes, + ...data.productType.variantAttributes + ].find(attribute => attribute.id === params.id).name, + "..." + )} + confirmButtonState={unassignAttribute.opts.status} + onClose={closeModal} + onConfirm={handleAttributeUnassign} + open={params.action === "unassign-attribute"} + productTypeName={maybe(() => data.productType.name, "...")} + /> + + ); + }} + + ); + }} + ); }; export default ProductTypeUpdate; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index f0efcfd24..3f2ff5cc4 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.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 from "@saleor/components/Metadata/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -20,6 +21,7 @@ import { SearchCollections_search_edges_node } from "@saleor/searches/types/Sear import { FetchMoreProps, ListActions } from "@saleor/types"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import { convertFromRaw, RawDraftContentState } from "draft-js"; import { diff } from "fast-array-diff"; import React from "react"; @@ -155,6 +157,12 @@ export const ProductUpdatePage: React.FC = ({ getChoices(maybe(() => product.collections, [])) ); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const initialData = getProductUpdatePageFormData(product, variants); const initialDescription = maybe(() => JSON.parse(product.descriptionJson) @@ -167,11 +175,18 @@ export const ProductUpdatePage: React.FC = ({ const hasVariants = maybe(() => product.productType.hasVariants, false); const handleSubmit = (data: ProductUpdatePageFormData) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + if (product.productType.hasVariants) { onSubmit({ ...data, addStocks: [], attributes, + metadata, + privateMetadata, removeStocks: [], updateStocks: [] }); @@ -188,6 +203,8 @@ export const ProductUpdatePage: React.FC = ({ stockDiff.added.some(addedStock => addedStock === stock.id) ), attributes, + metadata, + privateMetadata, removeStocks: stockDiff.removed, updateStocks: stocks.filter( stock => !stockDiff.added.some(addedStock => addedStock === stock.id) @@ -224,6 +241,7 @@ export const ProductUpdatePage: React.FC = ({ attributes, triggerChange ); + const changeMetadata = makeMetadataChangeHandler(change); return ( <> @@ -347,6 +365,8 @@ export const ProductUpdatePage: React.FC = ({ "Add search engine title and description to make this product easier to find" })} /> + +
= ({ const [isModalOpened, setModalStatus] = React.useState(false); const toggleModal = () => setModalStatus(!isModalOpened); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + const variantImages = maybe(() => variant.images.map(image => image.id), []); const productImages = maybe(() => variant.product.images.sort((prev, next) => @@ -114,7 +124,9 @@ const ProductVariantPage: React.FC = ({ const initialForm: ProductVariantPageFormData = { costPrice: maybe(() => variant.costPrice.amount.toString(), ""), + metadata: variant?.metadata?.map(mapMetadataItemToInput), price: maybe(() => variant.price.amount.toString(), ""), + privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput), sku: maybe(() => variant.sku, ""), trackInventory: variant?.trackInventory, weight: variant?.weight?.value.toString() || "" @@ -124,6 +136,10 @@ const ProductVariantPage: React.FC = ({ const dataStocks = stocks.map(stock => stock.id); const variantStocks = variant.stocks.map(stock => stock.warehouse.id); const stockDiff = diff(variantStocks, dataStocks); + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; onSubmit({ ...data, @@ -131,6 +147,8 @@ const ProductVariantPage: React.FC = ({ stockDiff.added.some(addedStock => addedStock === stock.id) ), attributes, + metadata, + privateMetadata, removeStocks: stockDiff.removed, updateStocks: stocks.filter( stock => !stockDiff.added.some(addedStock => addedStock === stock.id) @@ -152,6 +170,8 @@ const ProductVariantPage: React.FC = ({ triggerChange(); }; + const changeMetadata = makeMetadataChangeHandler(change); + return ( <> @@ -235,6 +255,8 @@ const ProductVariantPage: React.FC = ({ removeStock(id); }} /> + +
({ url: placeholderImage } ], + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], name: "Extended Hard", price: { __typename: "Money", amount: 100, currency: "USD" }, + privateMetadata: [], product: { __typename: "Product" as "Product", id: "prod1", diff --git a/src/products/types/ProductCreate.ts b/src/products/types/ProductCreate.ts index bdf72cced..9bfec00b3 100644 --- a/src/products/types/ProductCreate.ts +++ b/src/products/types/ProductCreate.ts @@ -99,6 +99,18 @@ export interface ProductCreate_productCreate_product_pricing { priceRangeUndiscounted: ProductCreate_productCreate_product_pricing_priceRangeUndiscounted | null; } +export interface ProductCreate_productCreate_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductCreate_productCreate_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductCreate_productCreate_product_category { __typename: "Category"; id: string; @@ -186,6 +198,8 @@ export interface ProductCreate_productCreate_product { attributes: ProductCreate_productCreate_product_attributes[]; productType: ProductCreate_productCreate_product_productType; pricing: ProductCreate_productCreate_product_pricing | null; + metadata: (ProductCreate_productCreate_product_metadata | null)[]; + privateMetadata: (ProductCreate_productCreate_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index c5292b43f..b0708046d 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -93,6 +93,18 @@ export interface ProductDetails_product_pricing { priceRangeUndiscounted: ProductDetails_product_pricing_priceRangeUndiscounted | null; } +export interface ProductDetails_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductDetails_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductDetails_product_category { __typename: "Category"; id: string; @@ -180,6 +192,8 @@ export interface ProductDetails_product { attributes: ProductDetails_product_attributes[]; productType: ProductDetails_product_productType; pricing: ProductDetails_product_pricing | null; + metadata: (ProductDetails_product_metadata | null)[]; + privateMetadata: (ProductDetails_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/products/types/ProductImageCreate.ts b/src/products/types/ProductImageCreate.ts index b9b79f8ba..682c725d0 100644 --- a/src/products/types/ProductImageCreate.ts +++ b/src/products/types/ProductImageCreate.ts @@ -99,6 +99,18 @@ export interface ProductImageCreate_productImageCreate_product_pricing { priceRangeUndiscounted: ProductImageCreate_productImageCreate_product_pricing_priceRangeUndiscounted | null; } +export interface ProductImageCreate_productImageCreate_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductImageCreate_productImageCreate_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductImageCreate_productImageCreate_product_category { __typename: "Category"; id: string; @@ -186,6 +198,8 @@ export interface ProductImageCreate_productImageCreate_product { attributes: ProductImageCreate_productImageCreate_product_attributes[]; productType: ProductImageCreate_productImageCreate_product_productType; pricing: ProductImageCreate_productImageCreate_product_pricing | null; + metadata: (ProductImageCreate_productImageCreate_product_metadata | null)[]; + privateMetadata: (ProductImageCreate_productImageCreate_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/products/types/ProductImageUpdate.ts b/src/products/types/ProductImageUpdate.ts index a207ae2b1..cb7249929 100644 --- a/src/products/types/ProductImageUpdate.ts +++ b/src/products/types/ProductImageUpdate.ts @@ -99,6 +99,18 @@ export interface ProductImageUpdate_productImageUpdate_product_pricing { priceRangeUndiscounted: ProductImageUpdate_productImageUpdate_product_pricing_priceRangeUndiscounted | null; } +export interface ProductImageUpdate_productImageUpdate_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductImageUpdate_productImageUpdate_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductImageUpdate_productImageUpdate_product_category { __typename: "Category"; id: string; @@ -186,6 +198,8 @@ export interface ProductImageUpdate_productImageUpdate_product { attributes: ProductImageUpdate_productImageUpdate_product_attributes[]; productType: ProductImageUpdate_productImageUpdate_product_productType; pricing: ProductImageUpdate_productImageUpdate_product_pricing | null; + metadata: (ProductImageUpdate_productImageUpdate_product_metadata | null)[]; + privateMetadata: (ProductImageUpdate_productImageUpdate_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/products/types/ProductUpdate.ts b/src/products/types/ProductUpdate.ts index 96c52679a..a495a4483 100644 --- a/src/products/types/ProductUpdate.ts +++ b/src/products/types/ProductUpdate.ts @@ -99,6 +99,18 @@ export interface ProductUpdate_productUpdate_product_pricing { priceRangeUndiscounted: ProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null; } +export interface ProductUpdate_productUpdate_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductUpdate_productUpdate_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductUpdate_productUpdate_product_category { __typename: "Category"; id: string; @@ -186,6 +198,8 @@ export interface ProductUpdate_productUpdate_product { attributes: ProductUpdate_productUpdate_product_attributes[]; productType: ProductUpdate_productUpdate_product_productType; pricing: ProductUpdate_productUpdate_product_pricing | null; + metadata: (ProductUpdate_productUpdate_product_metadata | null)[]; + privateMetadata: (ProductUpdate_productUpdate_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; diff --git a/src/products/types/ProductVariantDetails.ts b/src/products/types/ProductVariantDetails.ts index fa14f8aaa..425cbc21d 100644 --- a/src/products/types/ProductVariantDetails.ts +++ b/src/products/types/ProductVariantDetails.ts @@ -8,6 +8,18 @@ import { WeightUnitsEnum } from "./../../types/globalTypes"; // GraphQL query operation: ProductVariantDetails // ==================================================== +export interface ProductVariantDetails_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductVariantDetails_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface ProductVariantDetails_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -114,6 +126,8 @@ export interface ProductVariantDetails_productVariant_weight { export interface ProductVariantDetails_productVariant { __typename: "ProductVariant"; id: string; + metadata: (ProductVariantDetails_productVariant_metadata | null)[]; + privateMetadata: (ProductVariantDetails_productVariant_privateMetadata | null)[]; attributes: ProductVariantDetails_productVariant_attributes[]; costPrice: ProductVariantDetails_productVariant_costPrice | null; images: (ProductVariantDetails_productVariant_images | null)[] | null; diff --git a/src/products/types/SimpleProductUpdate.ts b/src/products/types/SimpleProductUpdate.ts index ec0f06ad3..4a3ecf4ad 100644 --- a/src/products/types/SimpleProductUpdate.ts +++ b/src/products/types/SimpleProductUpdate.ts @@ -99,6 +99,18 @@ export interface SimpleProductUpdate_productUpdate_product_pricing { priceRangeUndiscounted: SimpleProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null; } +export interface SimpleProductUpdate_productUpdate_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface SimpleProductUpdate_productUpdate_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface SimpleProductUpdate_productUpdate_product_category { __typename: "Category"; id: string; @@ -186,6 +198,8 @@ export interface SimpleProductUpdate_productUpdate_product { attributes: SimpleProductUpdate_productUpdate_product_attributes[]; productType: SimpleProductUpdate_productUpdate_product_productType; pricing: SimpleProductUpdate_productUpdate_product_pricing | null; + metadata: (SimpleProductUpdate_productUpdate_product_metadata | null)[]; + privateMetadata: (SimpleProductUpdate_productUpdate_product_privateMetadata | null)[]; name: string; descriptionJson: any; seoTitle: string | null; @@ -215,6 +229,18 @@ export interface SimpleProductUpdate_productVariantUpdate_errors { field: string | null; } +export interface SimpleProductUpdate_productVariantUpdate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface SimpleProductUpdate_productVariantUpdate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface SimpleProductUpdate_productVariantUpdate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -321,6 +347,8 @@ export interface SimpleProductUpdate_productVariantUpdate_productVariant_weight export interface SimpleProductUpdate_productVariantUpdate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (SimpleProductUpdate_productVariantUpdate_productVariant_metadata | null)[]; + privateMetadata: (SimpleProductUpdate_productVariantUpdate_productVariant_privateMetadata | null)[]; attributes: SimpleProductUpdate_productVariantUpdate_productVariant_attributes[]; costPrice: SimpleProductUpdate_productVariantUpdate_productVariant_costPrice | null; images: (SimpleProductUpdate_productVariantUpdate_productVariant_images | null)[] | null; @@ -346,6 +374,18 @@ export interface SimpleProductUpdate_productVariantStocksCreate_errors { index: number | null; } +export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -452,6 +492,8 @@ export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_w export interface SimpleProductUpdate_productVariantStocksCreate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (SimpleProductUpdate_productVariantStocksCreate_productVariant_metadata | null)[]; + privateMetadata: (SimpleProductUpdate_productVariantStocksCreate_productVariant_privateMetadata | null)[]; attributes: SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes[]; costPrice: SimpleProductUpdate_productVariantStocksCreate_productVariant_costPrice | null; images: (SimpleProductUpdate_productVariantStocksCreate_productVariant_images | null)[] | null; @@ -476,6 +518,18 @@ export interface SimpleProductUpdate_productVariantStocksDelete_errors { field: string | null; } +export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -582,6 +636,8 @@ export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_w export interface SimpleProductUpdate_productVariantStocksDelete_productVariant { __typename: "ProductVariant"; id: string; + metadata: (SimpleProductUpdate_productVariantStocksDelete_productVariant_metadata | null)[]; + privateMetadata: (SimpleProductUpdate_productVariantStocksDelete_productVariant_privateMetadata | null)[]; attributes: SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes[]; costPrice: SimpleProductUpdate_productVariantStocksDelete_productVariant_costPrice | null; images: (SimpleProductUpdate_productVariantStocksDelete_productVariant_images | null)[] | null; @@ -607,6 +663,18 @@ export interface SimpleProductUpdate_productVariantStocksUpdate_errors { index: number | null; } +export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -713,6 +781,8 @@ export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_w export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_metadata | null)[]; + privateMetadata: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_privateMetadata | null)[]; attributes: SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes[]; costPrice: SimpleProductUpdate_productVariantStocksUpdate_productVariant_costPrice | null; images: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null; diff --git a/src/products/types/VariantCreate.ts b/src/products/types/VariantCreate.ts index e469ba5d0..888b46a09 100644 --- a/src/products/types/VariantCreate.ts +++ b/src/products/types/VariantCreate.ts @@ -14,6 +14,18 @@ export interface VariantCreate_productVariantCreate_errors { field: string | null; } +export interface VariantCreate_productVariantCreate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface VariantCreate_productVariantCreate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface VariantCreate_productVariantCreate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -120,6 +132,8 @@ export interface VariantCreate_productVariantCreate_productVariant_weight { export interface VariantCreate_productVariantCreate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (VariantCreate_productVariantCreate_productVariant_metadata | null)[]; + privateMetadata: (VariantCreate_productVariantCreate_productVariant_privateMetadata | null)[]; attributes: VariantCreate_productVariantCreate_productVariant_attributes[]; costPrice: VariantCreate_productVariantCreate_productVariant_costPrice | null; images: (VariantCreate_productVariantCreate_productVariant_images | null)[] | null; diff --git a/src/products/types/VariantImageAssign.ts b/src/products/types/VariantImageAssign.ts index b6d39774e..1980f0df5 100644 --- a/src/products/types/VariantImageAssign.ts +++ b/src/products/types/VariantImageAssign.ts @@ -14,6 +14,18 @@ export interface VariantImageAssign_variantImageAssign_errors { field: string | null; } +export interface VariantImageAssign_variantImageAssign_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface VariantImageAssign_variantImageAssign_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface VariantImageAssign_variantImageAssign_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -120,6 +132,8 @@ export interface VariantImageAssign_variantImageAssign_productVariant_weight { export interface VariantImageAssign_variantImageAssign_productVariant { __typename: "ProductVariant"; id: string; + metadata: (VariantImageAssign_variantImageAssign_productVariant_metadata | null)[]; + privateMetadata: (VariantImageAssign_variantImageAssign_productVariant_privateMetadata | null)[]; attributes: VariantImageAssign_variantImageAssign_productVariant_attributes[]; costPrice: VariantImageAssign_variantImageAssign_productVariant_costPrice | null; images: (VariantImageAssign_variantImageAssign_productVariant_images | null)[] | null; diff --git a/src/products/types/VariantImageUnassign.ts b/src/products/types/VariantImageUnassign.ts index acc83d810..2b3b63ebb 100644 --- a/src/products/types/VariantImageUnassign.ts +++ b/src/products/types/VariantImageUnassign.ts @@ -14,6 +14,18 @@ export interface VariantImageUnassign_variantImageUnassign_errors { field: string | null; } +export interface VariantImageUnassign_variantImageUnassign_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface VariantImageUnassign_variantImageUnassign_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface VariantImageUnassign_variantImageUnassign_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -120,6 +132,8 @@ export interface VariantImageUnassign_variantImageUnassign_productVariant_weight export interface VariantImageUnassign_variantImageUnassign_productVariant { __typename: "ProductVariant"; id: string; + metadata: (VariantImageUnassign_variantImageUnassign_productVariant_metadata | null)[]; + privateMetadata: (VariantImageUnassign_variantImageUnassign_productVariant_privateMetadata | null)[]; attributes: VariantImageUnassign_variantImageUnassign_productVariant_attributes[]; costPrice: VariantImageUnassign_variantImageUnassign_productVariant_costPrice | null; images: (VariantImageUnassign_variantImageUnassign_productVariant_images | null)[] | null; diff --git a/src/products/types/VariantUpdate.ts b/src/products/types/VariantUpdate.ts index 4858e750a..9ebe6a027 100644 --- a/src/products/types/VariantUpdate.ts +++ b/src/products/types/VariantUpdate.ts @@ -14,6 +14,18 @@ export interface VariantUpdate_productVariantUpdate_errors { field: string | null; } +export interface VariantUpdate_productVariantUpdate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface VariantUpdate_productVariantUpdate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface VariantUpdate_productVariantUpdate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -120,6 +132,8 @@ export interface VariantUpdate_productVariantUpdate_productVariant_weight { export interface VariantUpdate_productVariantUpdate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (VariantUpdate_productVariantUpdate_productVariant_metadata | null)[]; + privateMetadata: (VariantUpdate_productVariantUpdate_productVariant_privateMetadata | null)[]; attributes: VariantUpdate_productVariantUpdate_productVariant_attributes[]; costPrice: VariantUpdate_productVariantUpdate_productVariant_costPrice | null; images: (VariantUpdate_productVariantUpdate_productVariant_images | null)[] | null; @@ -145,6 +159,18 @@ export interface VariantUpdate_productVariantStocksUpdate_errors { index: number | null; } +export interface VariantUpdate_productVariantStocksUpdate_productVariant_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values { __typename: "AttributeValue"; id: string; @@ -251,6 +277,8 @@ export interface VariantUpdate_productVariantStocksUpdate_productVariant_weight export interface VariantUpdate_productVariantStocksUpdate_productVariant { __typename: "ProductVariant"; id: string; + metadata: (VariantUpdate_productVariantStocksUpdate_productVariant_metadata | null)[]; + privateMetadata: (VariantUpdate_productVariantStocksUpdate_productVariant_privateMetadata | null)[]; attributes: VariantUpdate_productVariantStocksUpdate_productVariant_attributes[]; costPrice: VariantUpdate_productVariantStocksUpdate_productVariant_costPrice | null; images: (VariantUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null; diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 1db290718..f17aaf339 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -1,3 +1,4 @@ +import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; @@ -10,6 +11,7 @@ import { } from "@saleor/products/types/ProductDetails"; import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes"; import { StockInput } from "@saleor/types/globalTypes"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { RawDraftContentState } from "draft-js"; import { ProductAttributeInput } from "../components/ProductAttributes"; @@ -168,7 +170,7 @@ export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] { ); } -export interface ProductUpdatePageFormData { +export interface ProductUpdatePageFormData extends MetadataFormData { basePrice: number; category: string | null; collections: string[]; @@ -198,7 +200,9 @@ export function getProductUpdatePageFormData( ), description: maybe(() => JSON.parse(product.descriptionJson)), isPublished: maybe(() => product.isPublished, false), + metadata: product?.metadata?.map(mapMetadataItemToInput), name: maybe(() => product.name, ""), + privateMetadata: product?.privateMetadata?.map(mapMetadataItemToInput), publicationDate: maybe(() => product.publicationDate, ""), seoDescription: maybe(() => product.seoDescription, ""), seoTitle: maybe(() => product.seoTitle, ""), diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index e25e4df6b..76bd5bd72 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -23,6 +23,11 @@ import { import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; import { useWarehouseList } from "@saleor/warehouses/queries"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -81,6 +86,8 @@ export const ProductUpdate: React.FC = ({ id, params }) => { } }); const shop = useShop(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const { data, loading, refetch } = useProductDetails({ displayLoader: true, @@ -181,10 +188,15 @@ export const ProductUpdate: React.FC = ({ id, params }) => { deleteProductImage({ variables: { id } }); const handleImageEdit = (imageId: string) => () => navigate(productImageUrl(id, imageId)); - const handleSubmit = createUpdateHandler( + const handleSubmit = createMetadataUpdateHandler( product, - variables => updateProduct({ variables }), - variables => updateSimpleProduct({ variables }) + createUpdateHandler( + product, + variables => updateProduct({ variables }), + variables => updateSimpleProduct({ variables }) + ), + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) ); const handleImageUpload = createImageUploadHandler(id, variables => createProductImage({ variables }) diff --git a/src/products/views/ProductUpdate/handlers.ts b/src/products/views/ProductUpdate/handlers.ts index 30193868e..e736a98d7 100644 --- a/src/products/views/ProductUpdate/handlers.ts +++ b/src/products/views/ProductUpdate/handlers.ts @@ -1,20 +1,34 @@ +import { BulkStockErrorFragment } from "@saleor/fragments/types/BulkStockErrorFragment"; +import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { StockErrorFragment } from "@saleor/fragments/types/StockErrorFragment"; import { decimal, weight } from "@saleor/misc"; import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage"; import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import { ProductImageCreateVariables } from "@saleor/products/types/ProductImageCreate"; import { ProductImageReorderVariables } from "@saleor/products/types/ProductImageReorder"; -import { ProductUpdateVariables } from "@saleor/products/types/ProductUpdate"; -import { SimpleProductUpdateVariables } from "@saleor/products/types/SimpleProductUpdate"; +import { + ProductUpdate, + ProductUpdateVariables +} from "@saleor/products/types/ProductUpdate"; +import { + SimpleProductUpdate, + SimpleProductUpdateVariables +} from "@saleor/products/types/SimpleProductUpdate"; import { mapFormsetStockToStockInput } from "@saleor/products/utils/data"; import { ReorderEvent } from "@saleor/types"; +import { MutationFetchResult } from "react-apollo"; import { arrayMove } from "react-sortable-hoc"; export function createUpdateHandler( product: ProductDetails_product, - updateProduct: (variables: ProductUpdateVariables) => void, - updateSimpleProduct: (variables: SimpleProductUpdateVariables) => void + updateProduct: ( + variables: ProductUpdateVariables + ) => Promise>, + updateSimpleProduct: ( + variables: SimpleProductUpdateVariables + ) => Promise> ) { - return (data: ProductUpdatePageSubmitData) => { + return async (data: ProductUpdatePageSubmitData) => { const productVariables: ProductUpdateVariables = { attributes: data.attributes.map(attribute => ({ id: attribute.id, @@ -36,10 +50,15 @@ export function createUpdateHandler( } }; + let errors: Array< + ProductErrorFragment | StockErrorFragment | BulkStockErrorFragment + >; + if (product.productType.hasVariants) { - updateProduct(productVariables); + const result = await updateProduct(productVariables); + errors = result.data.productUpdate.errors; } else { - updateSimpleProduct({ + const result = await updateSimpleProduct({ ...productVariables, addStocks: data.addStocks.map(mapFormsetStockToStockInput), deleteStocks: data.removeStocks, @@ -51,7 +70,16 @@ export function createUpdateHandler( updateStocks: data.updateStocks.map(mapFormsetStockToStockInput), weight: weight(data.weight) }); + errors = [ + ...result.data.productUpdate.errors, + ...result.data.productVariantStocksCreate.errors, + ...result.data.productVariantStocksDelete.errors, + ...result.data.productVariantStocksUpdate.errors, + ...result.data.productVariantUpdate.errors + ]; } + + return errors; }; } diff --git a/src/products/views/ProductVariant.tsx b/src/products/views/ProductVariant.tsx index 5b58d89bf..bce7b327b 100644 --- a/src/products/views/ProductVariant.tsx +++ b/src/products/views/ProductVariant.tsx @@ -6,6 +6,11 @@ import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; import { useWarehouseList } from "@saleor/warehouses/queries"; import React, { useEffect, useState } from "react"; import { useIntl } from "react-intl"; @@ -67,6 +72,8 @@ export const ProductVariant: React.FC = ({ id: variantId } }); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); const [openModal] = createDialogActionHandlers< ProductVariantEditUrlDialog, @@ -140,6 +147,39 @@ export const ProductVariant: React.FC = ({ } }; + const handleUpdate = async (data: ProductVariantPageSubmitData) => { + const result = await updateVariant({ + variables: { + addStocks: data.addStocks.map(mapFormsetStockToStockInput), + attributes: data.attributes.map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), + costPrice: decimal(data.costPrice), + id: variantId, + price: decimal(data.price), + removeStocks: data.removeStocks, + sku: data.sku, + stocks: data.updateStocks.map(mapFormsetStockToStockInput), + trackInventory: data.trackInventory, + weight: weight(data.weight) + } + }); + + return [ + ...result.data?.productVariantStocksCreate.errors, + ...result.data?.productVariantStocksDelete.errors, + ...result.data?.productVariantStocksUpdate.errors, + ...result.data?.productVariantUpdate.errors + ]; + }; + const handleSubmit = createMetadataUpdateHandler( + data?.productVariant, + handleUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + return ( <> @@ -158,25 +198,7 @@ export const ProductVariant: React.FC = ({ onBack={handleBack} onDelete={() => openModal("remove")} onImageSelect={handleImageSelect} - onSubmit={(data: ProductVariantPageSubmitData) => - updateVariant({ - variables: { - addStocks: data.addStocks.map(mapFormsetStockToStockInput), - attributes: data.attributes.map(attribute => ({ - id: attribute.id, - values: [attribute.value] - })), - costPrice: decimal(data.costPrice), - id: variantId, - price: decimal(data.price), - removeStocks: data.removeStocks, - sku: data.sku, - stocks: data.updateStocks.map(mapFormsetStockToStockInput), - trackInventory: data.trackInventory, - weight: weight(data.weight) - } - }) - } + onSubmit={handleSubmit} onVariantClick={variantId => { navigate(productVariantEditUrl(productId, variantId)); }} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index d184d5765..1d5c30a43 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -3333,6 +3333,1086 @@ exports[`Storyshots Generics / Link with choices default 1`] = ` `; +exports[`Storyshots Generics / Metadata default 1`] = ` +
+
+
+ + Metadata + +
+
+
+
+ + + + + + + + + + + + +