From fc02fce701d0d79279b9d7c728ae6b3fd042ef5a Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Thu, 19 Nov 2020 15:42:14 +0100 Subject: [PATCH] Page types (#807) * Create attribute class selector * Use ProductAttributeType to check if product is simple or with variants * Allow attribute class selection only during its creation * Update attribute type selection translations * Show only product attributes in columns picker on product list view * Cleanups in Attribute Organization component * Create Page Types list page * Create content management section in settings * Implement page types list view * Remove unused imports from page type list * Updatte page type list style * Remove legacy code from page type list component * Update PageTypeListPage component * Create Page Types details page * Fix page type attribute reordering * Implement PageType create view * Implement PageType update view * gUpdate page type details components * Fix page type update component * Update page type components stories * Update page type errors handling * Update page type details view * Create Page Types details page * Implement PageType create view * Update product attribute assignment mutations * Add page types attribute assignment mutations * Add page types attribute assignment handling * Temporarily fix page create mutation * Update page type error messages * Remove legacy storybook page type stories * Update attribute assignment dialogs stories * Update page type details error handling * Update props for page type components * Create attribute class selector * Implement page types list view * Add page type selector on page create and details views * Add attributes list to page details views * Update page types list * Use attribute errors for attributes muatations * Save attribute values on page create and update * Update messages for page view * Update page attributes fragment * Use AttributeError in AttributeBulkDelete * Update page type and its attribute selection * Handle page types deleting * Update page types deleting messages * Handle page types attribute reorder * Fix PageOrganizeContent component types * Update graphqql types * Fix page fixture * Update messages * Update test snapshots * Pass pageTypes to PageForm * Update changelog with page type addition note * Update package-lock * Update test snapshots * Fix malformed generated type * Update messages after rebase --- CHANGELOG.md | 2 + locale/defaultMessages.json | 239 +- package-lock.json | 41 +- schema.graphql | 329 +- .../AttributeDetails/AttributeDetails.tsx | 11 +- .../AttributeOrganization.tsx | 104 + .../components/AttributeOrganization/index.ts | 2 + .../AttributePage/AttributePage.tsx | 24 +- .../AttributeProperties.tsx | 87 +- .../AttributeValueEditDialog.tsx | 4 +- src/attributes/errors.ts | 18 +- src/attributes/fixtures.ts | 14 + src/attributes/mutations.ts | 50 +- src/attributes/types/AttributeBulkDelete.ts | 6 +- src/attributes/types/AttributeCreate.ts | 7 +- src/attributes/types/AttributeDelete.ts | 6 +- src/attributes/types/AttributeDetails.ts | 3 +- src/attributes/types/AttributeList.ts | 3 +- src/attributes/types/AttributeUpdate.ts | 7 +- src/attributes/types/AttributeValueCreate.ts | 7 +- src/attributes/types/AttributeValueDelete.ts | 7 +- src/attributes/types/AttributeValueReorder.ts | 6 +- src/attributes/types/AttributeValueUpdate.ts | 7 +- .../views/AttributeCreate/AttributeCreate.tsx | 16 +- .../AttributeDetails/AttributeDetails.tsx | 4 +- .../AssignAttributeDialog.stories.tsx} | 6 +- .../AssignAttributeDialog.tsx | 5 +- .../components/AssignAttributeDialog/index.ts | 0 .../AttributeUnassignDialog.stories.tsx | 21 + .../AttributeUnassignDialog.tsx | 45 + .../AttributeUnassignDialog/index.ts | 2 + .../BulkAttributeUnassignDialog.stories.tsx | 21 + .../BulkAttributeUnassignDialog.tsx | 47 + .../BulkAttributeUnassignDialog/index.ts | 2 + .../RadioGroupField/RadioGroupField.tsx | 2 +- src/configuration/index.tsx | 39 +- src/fragments/attributes.ts | 9 + src/fragments/errors.ts | 15 + src/fragments/pageTypes.ts | 24 + src/fragments/pages.ts | 41 + .../types/AttributeDetailsFragment.ts | 3 +- src/fragments/types/AttributeErrorFragment.ts | 15 + src/fragments/types/AttributeFragment.ts | 3 + .../types/AvailableAttributeFragment.ts | 14 + src/fragments/types/MetadataFragment.ts | 2 +- src/fragments/types/PageAttributesFragment.ts | 68 + src/fragments/types/PageDetailsFragment.ts | 57 + .../types/PageErrorWithAttributesFragment.ts | 16 + .../types/PageTypeDetailsFragment.ts | 41 + src/fragments/types/PageTypeFragment.ts | 13 + .../types/ProductTypeDetailsFragment.ts | 4 +- src/icons/PageTypes.tsx | 16 + src/index.tsx | 6 + src/intl.ts | 4 + .../PageTypeAttributes/PageTypeAttributes.tsx | 191 + .../components/PageTypeAttributes/index.ts | 2 + .../PageTypeBulkDeleteDialog.tsx | 63 + .../PageTypeBulkDeleteDialog/index.ts | 2 + .../PageTypeCreatePage.stories.tsx | 34 + .../PageTypeCreatePage/PageTypeCreatePage.tsx | 114 + .../components/PageTypeCreatePage/index.ts | 2 + .../PageTypeDeleteDialog.tsx | 61 + .../components/PageTypeDeleteDialog/index.ts | 2 + .../PageTypeDetails/PageTypeDetails.tsx | 53 + .../components/PageTypeDetails/index.ts | 2 + .../PageTypeDetailsPage.stories.tsx | 60 + .../PageTypeDetailsPage.tsx | 183 + .../components/PageTypeDetailsPage/index.ts | 2 + .../components/PageTypeList/PageTypeList.tsx | 145 + .../components/PageTypeList/index.ts | 2 + .../PageTypeListPage.stories.tsx | 36 + .../PageTypeListPage/PageTypeListPage.tsx | 83 + .../components/PageTypeListPage/index.ts | 2 + src/pageTypes/fixtures.ts | 73 + .../useAvailablePageAttributeSearch/index.tsx | 72 + .../types/SearchAvailablePageAttributes.ts | 50 + src/pageTypes/index.tsx | 62 + src/pageTypes/mutations.ts | 161 + src/pageTypes/queries.ts | 63 + src/pageTypes/types/AssignPageAttribute.ts | 62 + .../types/PageTypeAttributeReorder.ts | 62 + src/pageTypes/types/PageTypeBulkDelete.ts | 26 + src/pageTypes/types/PageTypeCreate.ts | 61 + src/pageTypes/types/PageTypeDelete.ts | 32 + src/pageTypes/types/PageTypeDetails.ts | 50 + src/pageTypes/types/PageTypeList.ts | 48 + src/pageTypes/types/PageTypeUpdate.ts | 62 + src/pageTypes/types/UnassignPageAttribute.ts | 62 + src/pageTypes/urls.ts | 52 + src/pageTypes/views/PageTypeCreate.tsx | 76 + src/pageTypes/views/PageTypeDetails.tsx | 324 + .../views/PageTypeList/PageTypeList.tsx | 226 + src/pageTypes/views/PageTypeList/filters.ts | 32 + src/pageTypes/views/PageTypeList/index.ts | 2 + src/pageTypes/views/PageTypeList/sort.ts | 18 + .../views/PageTypeUpdate/PageTypeUpdate.tsx | 0 .../PageAttributes/PageAttributes.tsx | 247 + src/pages/components/PageAttributes/index.ts | 2 + .../PageDetailsPage/PageDetailsPage.tsx | 43 +- src/pages/components/PageDetailsPage/form.tsx | 73 +- .../PageOrganizeContent.tsx | 102 + .../components/PageOrganizeContent/index.ts | 2 + src/pages/fixtures.ts | 156 + src/pages/mutations.ts | 15 +- src/pages/types/PageCreate.ts | 60 +- src/pages/types/PageDetails.ts | 57 + src/pages/types/PageUpdate.ts | 58 +- src/pages/utils/data.ts | 35 + src/pages/utils/handlers.test.ts | 111 + src/pages/utils/handlers.ts | 56 + src/pages/views/PageCreate.tsx | 24 + src/pages/views/PageDetails.tsx | 4 + .../ProductTypeAttributeUnassignDialog.tsx | 51 - .../index.ts | 2 - .../ProductTypeAttributes.tsx | 10 +- ...ProductTypeBulkAttributeUnassignDialog.tsx | 53 - .../index.ts | 2 - .../ProductTypeDetailsPage.tsx | 17 +- .../containers/ProductTypeOperations.tsx | 44 +- src/productTypes/fixtures.ts | 11 +- .../types/SearchAvailableAttributes.ts | 50 - .../index.tsx | 22 +- .../types/SearchAvailableProductAttributes.ts | 50 + src/productTypes/mutations.ts | 49 +- src/productTypes/types/AssignAttribute.ts | 88 - .../types/AssignProductAttribute.ts | 90 + .../types/ProductTypeAttributeReorder.ts | 6 +- src/productTypes/types/ProductTypeCreate.ts | 4 +- src/productTypes/types/ProductTypeDetails.ts | 4 +- src/productTypes/types/ProductTypeUpdate.ts | 4 +- src/productTypes/types/UnassignAttribute.ts | 88 - .../types/UnassignProductAttribute.ts | 90 + .../views/ProductTypeUpdate/index.tsx | 66 +- src/products/queries.ts | 6 +- src/searches/types/SearchPageTypes.ts | 62 + src/searches/usePageTypeSearch.ts | 45 + .../__snapshots__/Stories.test.ts.snap | 5951 ++++++++++++++++- src/storybook/config.js | 3 - .../stories/attributes/AttributePage.tsx | 8 +- .../attributes/AttributeValueEditDialog.tsx | 8 +- .../stories/pages/PageDetailsPage.tsx | 1 + .../ProductTypeAttributeUnassignDialog.tsx | 20 - ...ProductTypeBulkAttributeUnassignDialog.tsx | 20 - src/types.ts | 1 + src/types/globalTypes.ts | 77 +- src/utils/errors/attribute.ts | 46 + src/utils/errors/page.ts | 21 +- src/utils/metadata/types/UpdateMetadata.ts | 2 +- .../metadata/types/UpdatePrivateMetadata.ts | 2 +- 149 files changed, 11407 insertions(+), 972 deletions(-) create mode 100644 src/attributes/components/AttributeOrganization/AttributeOrganization.tsx create mode 100644 src/attributes/components/AttributeOrganization/index.ts rename src/{storybook/stories/components/AssignAttributeDialog.tsx => components/AssignAttributeDialog/AssignAttributeDialog.stories.tsx} (90%) rename src/{productTypes => }/components/AssignAttributeDialog/AssignAttributeDialog.tsx (96%) rename src/{productTypes => }/components/AssignAttributeDialog/index.ts (100%) create mode 100644 src/components/AttributeUnassignDialog/AttributeUnassignDialog.stories.tsx create mode 100644 src/components/AttributeUnassignDialog/AttributeUnassignDialog.tsx create mode 100644 src/components/AttributeUnassignDialog/index.ts create mode 100644 src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.stories.tsx create mode 100644 src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.tsx create mode 100644 src/components/BulkAttributeUnassignDialog/index.ts create mode 100644 src/fragments/pageTypes.ts create mode 100644 src/fragments/types/AttributeErrorFragment.ts create mode 100644 src/fragments/types/AvailableAttributeFragment.ts create mode 100644 src/fragments/types/PageAttributesFragment.ts create mode 100644 src/fragments/types/PageErrorWithAttributesFragment.ts create mode 100644 src/fragments/types/PageTypeDetailsFragment.ts create mode 100644 src/fragments/types/PageTypeFragment.ts create mode 100644 src/icons/PageTypes.tsx create mode 100644 src/pageTypes/components/PageTypeAttributes/PageTypeAttributes.tsx create mode 100644 src/pageTypes/components/PageTypeAttributes/index.ts create mode 100644 src/pageTypes/components/PageTypeBulkDeleteDialog/PageTypeBulkDeleteDialog.tsx create mode 100644 src/pageTypes/components/PageTypeBulkDeleteDialog/index.ts create mode 100644 src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.stories.tsx create mode 100644 src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx create mode 100644 src/pageTypes/components/PageTypeCreatePage/index.ts create mode 100644 src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx create mode 100644 src/pageTypes/components/PageTypeDeleteDialog/index.ts create mode 100644 src/pageTypes/components/PageTypeDetails/PageTypeDetails.tsx create mode 100644 src/pageTypes/components/PageTypeDetails/index.ts create mode 100644 src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.stories.tsx create mode 100644 src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx create mode 100644 src/pageTypes/components/PageTypeDetailsPage/index.ts create mode 100644 src/pageTypes/components/PageTypeList/PageTypeList.tsx create mode 100644 src/pageTypes/components/PageTypeList/index.ts create mode 100644 src/pageTypes/components/PageTypeListPage/PageTypeListPage.stories.tsx create mode 100644 src/pageTypes/components/PageTypeListPage/PageTypeListPage.tsx create mode 100644 src/pageTypes/components/PageTypeListPage/index.ts create mode 100644 src/pageTypes/fixtures.ts create mode 100644 src/pageTypes/hooks/useAvailablePageAttributeSearch/index.tsx create mode 100644 src/pageTypes/hooks/useAvailablePageAttributeSearch/types/SearchAvailablePageAttributes.ts create mode 100644 src/pageTypes/index.tsx create mode 100644 src/pageTypes/mutations.ts create mode 100644 src/pageTypes/queries.ts create mode 100644 src/pageTypes/types/AssignPageAttribute.ts create mode 100644 src/pageTypes/types/PageTypeAttributeReorder.ts create mode 100644 src/pageTypes/types/PageTypeBulkDelete.ts create mode 100644 src/pageTypes/types/PageTypeCreate.ts create mode 100644 src/pageTypes/types/PageTypeDelete.ts create mode 100644 src/pageTypes/types/PageTypeDetails.ts create mode 100644 src/pageTypes/types/PageTypeList.ts create mode 100644 src/pageTypes/types/PageTypeUpdate.ts create mode 100644 src/pageTypes/types/UnassignPageAttribute.ts create mode 100644 src/pageTypes/urls.ts create mode 100644 src/pageTypes/views/PageTypeCreate.tsx create mode 100644 src/pageTypes/views/PageTypeDetails.tsx create mode 100644 src/pageTypes/views/PageTypeList/PageTypeList.tsx create mode 100644 src/pageTypes/views/PageTypeList/filters.ts create mode 100644 src/pageTypes/views/PageTypeList/index.ts create mode 100644 src/pageTypes/views/PageTypeList/sort.ts create mode 100644 src/pageTypes/views/PageTypeUpdate/PageTypeUpdate.tsx create mode 100644 src/pages/components/PageAttributes/PageAttributes.tsx create mode 100644 src/pages/components/PageAttributes/index.ts create mode 100644 src/pages/components/PageOrganizeContent/PageOrganizeContent.tsx create mode 100644 src/pages/components/PageOrganizeContent/index.ts create mode 100644 src/pages/utils/data.ts create mode 100644 src/pages/utils/handlers.test.ts create mode 100644 src/pages/utils/handlers.ts delete mode 100644 src/productTypes/components/ProductTypeAttributeUnassignDialog/ProductTypeAttributeUnassignDialog.tsx delete mode 100644 src/productTypes/components/ProductTypeAttributeUnassignDialog/index.ts delete mode 100644 src/productTypes/components/ProductTypeBulkAttributeUnassignDialog/ProductTypeBulkAttributeUnassignDialog.tsx delete mode 100644 src/productTypes/components/ProductTypeBulkAttributeUnassignDialog/index.ts delete mode 100644 src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes.ts rename src/productTypes/hooks/{useAvailableAttributeSearch => useAvailableProductAttributeSearch}/index.tsx (73%) create mode 100644 src/productTypes/hooks/useAvailableProductAttributeSearch/types/SearchAvailableProductAttributes.ts delete mode 100644 src/productTypes/types/AssignAttribute.ts create mode 100644 src/productTypes/types/AssignProductAttribute.ts delete mode 100644 src/productTypes/types/UnassignAttribute.ts create mode 100644 src/productTypes/types/UnassignProductAttribute.ts create mode 100644 src/searches/types/SearchPageTypes.ts create mode 100644 src/searches/usePageTypeSearch.ts delete mode 100644 src/storybook/stories/productTypes/ProductTypeAttributeUnassignDialog.tsx delete mode 100644 src/storybook/stories/productTypes/ProductTypeBulkAttributeUnassignDialog.tsx create mode 100644 src/utils/errors/attribute.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8283ca4..5e4901494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable, unreleased changes to this project will be documented in this file. ## [Unreleased] +- Add Page Types - #807 by @orzechdev + # 2.11.1 - Support multiline text in plugin configuration secret field - #829 by @karolinakuzniewicz diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 0ca873b4f..596e15ca1 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -12,6 +12,9 @@ "configurationMenuNavigation": { "string": "Define how users can navigate through your store" }, + "configurationMenuPageTypes": { + "string": "Define types of content pages used in your store" + }, "configurationMenuPages": { "string": "Manage and add additional pages" }, @@ -208,6 +211,13 @@ "context": "vat not included in order price", "string": "does not apply" }, + "pageTypeCreateHeader": { + "context": "window title", + "string": "Create Page Type" + }, + "pageTypeInputLabel": { + "string": "Select content type" + }, "productExportFieldCategory": { "context": "product field", "string": "Category" @@ -808,6 +818,24 @@ "context": "attribute's label'", "string": "Default Label" }, + "src_dot_attributes_dot_components_dot_AttributeOrganization_dot_205334083": { + "string": "Define where this attribute should be used in Saleor system" + }, + "src_dot_attributes_dot_components_dot_AttributeOrganization_dot_3829816432": { + "context": "section header", + "string": "Organization" + }, + "src_dot_attributes_dot_components_dot_AttributeOrganization_dot_3835822980": { + "string": "Attribute Class" + }, + "src_dot_attributes_dot_components_dot_AttributeOrganization_dot_contentAttribute": { + "context": "attribute type", + "string": "Content Attribute" + }, + "src_dot_attributes_dot_components_dot_AttributeOrganization_dot_productAttribute": { + "context": "attribute type", + "string": "Product Attribute" + }, "src_dot_attributes_dot_components_dot_AttributePage_dot_2855501559": { "context": "page title", "string": "Create New Attribute" @@ -824,13 +852,16 @@ "context": "attribute properties regarding dashboard", "string": "Dashboard Properties" }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3135366329": { + "context": "attribute visibility in storefront", + "string": "Public" + }, "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3590282519": { "context": "attribute position in storefront filters", "string": "Position in faceted navigation" }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3876764312": { - "context": "attribute", - "string": "Visible on Product Page in Storefront" + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3758203740": { + "string": "If enabled, attribute will be accessible to customers." }, "src_dot_attributes_dot_components_dot_AttributeProperties_dot_4048785456": { "string": "If enabled this attribute can be used as a column in product table." @@ -1451,6 +1482,23 @@ "context": "section header", "string": "App Status" }, + "src_dot_components_dot_AssignAttributeDialog_dot_2173976534": { + "context": "button", + "string": "Assign attributes" + }, + "src_dot_components_dot_AssignAttributeDialog_dot_3922579741": { + "context": "dialog header", + "string": "Assign Attribute" + }, + "src_dot_components_dot_AssignAttributeDialog_dot_4205644805": { + "string": "No results found" + }, + "src_dot_components_dot_AssignAttributeDialog_dot_524117994": { + "string": "Search by attribute name" + }, + "src_dot_components_dot_AssignAttributeDialog_dot_902296540": { + "string": "Search Attributes" + }, "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": { "context": "dialog header", "string": "Assign Category" @@ -1489,6 +1537,9 @@ "context": "dialog header", "string": "Assign Product" }, + "src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": { + "string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?" + }, "src_dot_components_dot_AutocompleteSelectMenu_dot_2332404293": { "string": "No results" }, @@ -1528,6 +1579,10 @@ "context": "product unavailability", "string": "Unavailable for purchase" }, + "src_dot_components_dot_BulkAttributeUnassignDialog_dot_3177750460": { + "context": "unassign multiple attributes from item", + "string": "{counter,plural,one{Are you sure you want to unassign this attribute from {itemTypeName}?} other{Are you sure you want to unassign {attributeQuantity} attributes from {itemTypeName}?}}" + }, "src_dot_components_dot_ChannelsAvailabilityContent_dot_1528830621": { "string": "Select channels you want for {contentType} to be available on" }, @@ -2076,6 +2131,9 @@ "src_dot_configuration_dot_3140151600": { "string": "Staff Settings" }, + "src_dot_configuration_dot_3351299924": { + "string": "Content Management" + }, "src_dot_configuration_dot_3655543906": { "string": "Product Settings" }, @@ -3699,10 +3757,136 @@ "src_dot_orders_dot_views_dot_OrderList_dot_1738939038": { "string": "Order draft successfully created" }, + "src_dot_pageTypes": { + "context": "page types section name", + "string": "Page Types" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeAttributes_dot_1192828581": { + "string": "No attributes found" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeAttributes_dot_1228425832": { + "string": "Attribute name" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeAttributes_dot_1656462109": { + "context": "button", + "string": "Assign attribute" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeAttributes_dot_3478065224": { + "context": "attribute internal name", + "string": "Slug" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeAttributes_dot_613275198": { + "context": "section header", + "string": "Content Attributes" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeBulkDeleteDialog_dot_4266703515": { + "context": "delete page types with its pages", + "string": "{counter,plural,one{Page Type you want to delete is used by some pages. Deleting this page type will also delete those pages. Are you sure you want to delete this page type? After doing so you won’t be able to revert changes.} other{Page Types you want to delete are used by some pages. Deleting these page types will also delete those pages. Are you sure you want to delete {displayQuantity} page types? After doing so you won’t be able to revert changes.}}" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeBulkDeleteDialog_dot_69987032": { + "context": "dialog header", + "string": "Delete Page Types" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeBulkDeleteDialog_dot_8271141": { + "context": "delete page types", + "string": "{counter,plural,one{Are you sure you want to delete this page type? After doing so you won’t be able to revert changes.} other{Are you sure you want to delete {displayQuantity} page types? After doing so you won’t be able to revert changes.}}" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeCreatePage_dot_1105469372": { + "string": "These are general information about this Content Type." + }, + "src_dot_pageTypes_dot_components_dot_PageTypeCreatePage_dot_1509432322": { + "context": "section header", + "string": "Metadata" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeCreatePage_dot_4047854353": { + "context": "header", + "string": "Create Page Type" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_2364900868": { + "context": "delete page type with its pages", + "string": "Page Type you want to delete is used by some pages. Deleting this page type will also delete those pages. Are you sure you want to delete {name}? After doing so you won’t be able to revert changes." + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_3120835055": { + "context": "dialog header", + "string": "Delete Page Type" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_3734861990": { + "context": "delete page type", + "string": "Are you sure you want to delete {name}? After doing so you won’t be able to revert changes." + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDetailsPage_dot_1105469372": { + "string": "These are general information about this Content Type." + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDetailsPage_dot_1509432322": { + "context": "section header", + "string": "Metadata" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDetailsPage_dot_3466659718": { + "string": "This list shows all attributes that will be assigned to pages that have this page type assigned." + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDetailsPage_dot_613275198": { + "context": "section header", + "string": "Content Attributes" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeDetails_dot_1631499902": { + "string": "Content Type Name" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeListPage_dot_1793515137": { + "context": "button", + "string": "create page type" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeListPage_dot_1793828289": { + "string": "Search Page Type" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeListPage_dot_464566131": { + "context": "tab name", + "string": "All Page Types" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeList_dot_1631499902": { + "context": "page type name", + "string": "Content Type Name" + }, + "src_dot_pageTypes_dot_components_dot_PageTypeList_dot_2965257236": { + "string": "No page types found" + }, + "src_dot_pageTypes_dot_views_dot_2634056946": { + "context": "dialog header", + "string": "Unassign Attribute from Page Type" + }, + "src_dot_pageTypes_dot_views_dot_3442954831": { + "string": "Page type deleted" + }, + "src_dot_pageTypes_dot_views_dot_870815507": { + "context": "unassign attribute from page type, button", + "string": "Unassign" + }, + "src_dot_pageTypes_dot_views_dot_891131033": { + "context": "dialog header", + "string": "Unassign Attribute From Page Type" + }, + "src_dot_pageTypes_dot_views_dot_98187848": { + "string": "Successfully created page type" + }, "src_dot_pages": { "context": "pages section name", "string": "Pages" }, + "src_dot_pages_dot_components_dot_PageAttributes_dot_1071548120": { + "context": "number of page attributes", + "string": "{number} Attributes" + }, + "src_dot_pages_dot_components_dot_PageAttributes_dot_1148029984": { + "context": "attribute value", + "string": "Value" + }, + "src_dot_pages_dot_components_dot_PageAttributes_dot_1207761269": { + "context": "attribute values", + "string": "Values" + }, + "src_dot_pages_dot_components_dot_PageAttributes_dot_4153345096": { + "context": "page attributes, section header", + "string": "Attributes" + }, "src_dot_pages_dot_components_dot_PageDetailsPage_dot_1068617485": { "context": "page header", "string": "Create Page" @@ -3757,6 +3941,13 @@ "context": "page status", "string": "Not Published" }, + "src_dot_pages_dot_components_dot_PageOrganizeContent_dot_2959504520": { + "string": "Content type" + }, + "src_dot_pages_dot_components_dot_PageOrganizeContent_dot_590187004": { + "context": "section header", + "string": "Organize Content" + }, "src_dot_pages_dot_views_dot_1068617485": { "context": "header", "string": "Create Page" @@ -4061,30 +4252,6 @@ "context": "product types section name", "string": "Product Types" }, - "src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_2173976534": { - "context": "button", - "string": "Assign attributes" - }, - "src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_3922579741": { - "context": "dialog header", - "string": "Assign Attribute" - }, - "src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_4205644805": { - "string": "No results found" - }, - "src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_524117994": { - "string": "Search by attribute name" - }, - "src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_902296540": { - "string": "Search Attributes" - }, - "src_dot_productTypes_dot_components_dot_ProductTypeAttributeUnassignDialog_dot_404238501": { - "context": "dialog header", - "string": "Unassign Attribute From Product Type" - }, - "src_dot_productTypes_dot_components_dot_ProductTypeAttributeUnassignDialog_dot_722498450": { - "string": "Are you sure you want to unassign {attributeName} from {productTypeName}?" - }, "src_dot_productTypes_dot_components_dot_ProductTypeAttributes_dot_1192828581": { "string": "No attributes found" }, @@ -4107,14 +4274,6 @@ "context": "section header", "string": "Variant Attributes" }, - "src_dot_productTypes_dot_components_dot_ProductTypeBulkAttributeUnassignDialog_dot_2646729060": { - "context": "unassign multiple attributes from product type", - "string": "{counter,plural,one{Are you sure you want to unassign this attribute from {productTypeName}?} other{Are you sure you want to unassign {attributeQuantity} attributes from {productTypeName}?}}" - }, - "src_dot_productTypes_dot_components_dot_ProductTypeBulkAttributeUnassignDialog_dot_766918870": { - "context": "dialog header", - "string": "Unassign Attribute from Product Type" - }, "src_dot_productTypes_dot_components_dot_ProductTypeDeleteDialog_dot_2297471173": { "context": "delete product type", "string": "Are you sure you want to delete {name}?" @@ -4216,6 +4375,14 @@ "src_dot_productTypes_dot_views_dot_ProductTypeUpdate_dot_3512959355": { "string": "Product type deleted" }, + "src_dot_productTypes_dot_views_dot_ProductTypeUpdate_dot_404238501": { + "context": "dialog header", + "string": "Unassign Attribute From Product Type" + }, + "src_dot_productTypes_dot_views_dot_ProductTypeUpdate_dot_766918870": { + "context": "dialog header", + "string": "Unassign Attribute from Product Type" + }, "src_dot_productTypes_dot_views_dot_ProductTypeUpdate_dot_870815507": { "context": "unassign attribute from product type, button", "string": "Unassign" @@ -5901,7 +6068,7 @@ }, "src_dot_utils_dot_errors_dot_notFound": { "context": "error message", - "string": "Invoice not found" + "string": "Page not found." }, "src_dot_utils_dot_errors_dot_notReady": { "context": "error message", diff --git a/package-lock.json b/package-lock.json index a0b5a68f5..750790388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12580,7 +12580,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -12598,11 +12599,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12615,15 +12618,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -12726,7 +12732,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -12736,6 +12743,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -12748,17 +12756,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -12775,6 +12786,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -12847,7 +12859,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -12857,6 +12870,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -12932,7 +12946,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -12962,6 +12977,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -12979,6 +12995,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -13017,11 +13034,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, diff --git a/schema.graphql b/schema.graphql index 7daa390b2..bae1821a8 100644 --- a/schema.graphql +++ b/schema.graphql @@ -394,6 +394,7 @@ type Attribute implements Node & ObjectWithMetadata { inputType: AttributeInputTypeEnum name: String slug: String + type: AttributeTypeEnum values: [AttributeValue] valueRequired: Boolean! visibleInStorefront: Boolean! @@ -404,21 +405,10 @@ type Attribute implements Node & ObjectWithMetadata { storefrontSearchPosition: Int! } -type AttributeAssign { - errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") - productType: ProductType - productErrors: [ProductError!]! -} - -input AttributeAssignInput { - id: ID! - type: AttributeTypeEnum! -} - type AttributeBulkDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") count: Int! - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! } type AttributeCountableConnection { @@ -435,13 +425,14 @@ type AttributeCountableEdge { type AttributeCreate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! } input AttributeCreateInput { inputType: AttributeInputTypeEnum name: String! slug: String + type: AttributeTypeEnum! values: [AttributeValueCreateInput] valueRequired: Boolean isVariantOnly: Boolean @@ -454,10 +445,25 @@ input AttributeCreateInput { type AttributeDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! attribute: Attribute } +type AttributeError { + field: String + message: String + code: AttributeErrorCode! +} + +enum AttributeErrorCode { + ALREADY_EXISTS + GRAPHQL_ERROR + INVALID + NOT_FOUND + REQUIRED + UNIQUE +} + input AttributeFilterInput { valueRequired: Boolean isVariantOnly: Boolean @@ -467,6 +473,7 @@ input AttributeFilterInput { availableInGrid: Boolean search: String ids: [ID] + type: AttributeTypeEnum inCollection: ID inCategory: ID channel: String @@ -486,7 +493,7 @@ enum AttributeInputTypeEnum { type AttributeReorderValues { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! } enum AttributeSortField { @@ -526,20 +533,14 @@ type AttributeTranslation implements Node { } enum AttributeTypeEnum { - PRODUCT - VARIANT -} - -type AttributeUnassign { - errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") - productType: ProductType - productErrors: [ProductError!]! + PRODUCT_TYPE + PAGE_TYPE } type AttributeUpdate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! } input AttributeUpdateInput { @@ -568,13 +569,13 @@ type AttributeValue implements Node { type AttributeValueBulkDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") count: Int! - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! } type AttributeValueCreate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! attributeValue: AttributeValue } @@ -585,7 +586,7 @@ input AttributeValueCreateInput { type AttributeValueDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! attributeValue: AttributeValue } @@ -623,7 +624,7 @@ enum AttributeValueType { type AttributeValueUpdate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute - productErrors: [ProductError!]! + attributeErrors: [AttributeError!]! attributeValue: AttributeValue } @@ -1244,6 +1245,7 @@ type CollectionTranslation implements Node { type CollectionUpdate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") collectionErrors: [CollectionError!]! + productErrors: [ProductError!]! collection: Collection } @@ -2561,19 +2563,8 @@ type Mutation { shippingZoneDelete(id: ID!): ShippingZoneDelete shippingZoneBulkDelete(ids: [ID]!): ShippingZoneBulkDelete shippingZoneUpdate(id: ID!, input: ShippingZoneUpdateInput!): ShippingZoneUpdate - attributeCreate(input: AttributeCreateInput!): AttributeCreate - attributeDelete(id: ID!): AttributeDelete - attributeBulkDelete(ids: [ID]!): AttributeBulkDelete - attributeAssign(operations: [AttributeAssignInput]!, productTypeId: ID!): AttributeAssign - attributeUnassign(attributeIds: [ID]!, productTypeId: ID!): AttributeUnassign - attributeUpdate(id: ID!, input: AttributeUpdateInput!): AttributeUpdate - attributeTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): AttributeTranslate - attributeValueCreate(attribute: ID!, input: AttributeValueCreateInput!): AttributeValueCreate - attributeValueDelete(id: ID!): AttributeValueDelete - attributeValueBulkDelete(ids: [ID]!): AttributeValueBulkDelete - attributeValueUpdate(id: ID!, input: AttributeValueCreateInput!): AttributeValueUpdate - attributeValueTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): AttributeValueTranslate - attributeReorderValues(attributeId: ID!, moves: [ReorderInput]!): AttributeReorderValues + productAttributeAssign(operations: [ProductAttributeAssignInput]!, productTypeId: ID!): ProductAttributeAssign + productAttributeUnassign(attributeIds: [ID]!, productTypeId: ID!): ProductAttributeUnassign categoryCreate(input: CategoryInput!, parent: ID): CategoryCreate categoryDelete(id: ID!): CategoryDelete categoryBulkDelete(ids: [ID]!): CategoryBulkDelete @@ -2594,6 +2585,7 @@ type Mutation { productUpdate(id: ID!, input: ProductInput!): ProductUpdate productTranslate(id: ID!, input: TranslationInput!, languageCode: LanguageCodeEnum!): ProductTranslate productChannelListingUpdate(id: ID!, input: ProductChannelListingUpdateInput!): ProductChannelListingUpdate + productSetAvailabilityForPurchase(isAvailable: Boolean!, productId: ID!, startDate: Date): ProductSetAvailabilityForPurchase productImageCreate(input: ProductImageCreateInput!): ProductImageCreate productVariantReorder(moves: [ReorderInput]!, productId: ID!): ProductVariantReorder productImageDelete(id: ID!): ProductImageDelete @@ -2605,6 +2597,10 @@ type Mutation { productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: AttributeTypeEnum!): ProductTypeReorderAttributes + productTypeUpdateMetadata(id: ID!, input: MetaInput!): ProductTypeUpdateMeta @deprecated(reason: "Use the `updateMetadata` mutation instead. This field will be removed after 2020-07-31.") + productTypeClearMetadata(id: ID!, input: MetaPath!): ProductTypeClearMeta @deprecated(reason: "Use the `deleteMetadata` mutation instead. This field will be removed after 2020-07-31.") + productTypeUpdatePrivateMetadata(id: ID!, input: MetaInput!): ProductTypeUpdatePrivateMeta @deprecated(reason: "Use the `updatePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.") + productTypeClearPrivateMetadata(id: ID!, input: MetaPath!): ProductTypeClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.") digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate digitalContentDelete(variantId: ID!): DigitalContentDelete digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate @@ -2626,12 +2622,19 @@ type Mutation { paymentRefund(amount: PositiveDecimal, paymentId: ID!): PaymentRefund paymentVoid(paymentId: ID!): PaymentVoid paymentInitialize(gateway: String!, paymentData: JSONString): PaymentInitialize - pageCreate(input: PageInput!): PageCreate + pageCreate(input: PageCreateInput!): PageCreate pageDelete(id: ID!): PageDelete pageBulkDelete(ids: [ID]!): PageBulkDelete pageBulkPublish(ids: [ID]!, isPublished: Boolean!): PageBulkPublish pageUpdate(id: ID!, input: PageInput!): PageUpdate pageTranslate(id: ID!, input: PageTranslationInput!, languageCode: LanguageCodeEnum!): PageTranslate + pageTypeCreate(input: PageTypeCreateInput!): PageTypeCreate + pageTypeUpdate(id: ID, input: PageTypeUpdateInput!): PageTypeUpdate + pageTypeDelete(id: ID!): PageTypeDelete + pageTypeBulkDelete(ids: [ID!]!): PageTypeBulkDelete + pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign + pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign + pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes draftOrderComplete(id: ID!): DraftOrderComplete draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate draftOrderDelete(id: ID!): DraftOrderDelete @@ -2716,6 +2719,17 @@ type Mutation { channelDelete(id: ID!, input: ChannelDeleteInput!): ChannelDelete channelActivate(id: ID!): ChannelActivate channelDeactivate(id: ID!): ChannelDeactivate + attributeCreate(input: AttributeCreateInput!): AttributeCreate + attributeDelete(id: ID!): AttributeDelete + attributeUpdate(id: ID!, input: AttributeUpdateInput!): AttributeUpdate + attributeTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): AttributeTranslate + attributeBulkDelete(ids: [ID]!): AttributeBulkDelete + attributeValueBulkDelete(ids: [ID]!): AttributeValueBulkDelete + attributeValueCreate(attribute: ID!, input: AttributeValueCreateInput!): AttributeValueCreate + attributeValueDelete(id: ID!): AttributeValueDelete + attributeValueUpdate(id: ID!, input: AttributeValueCreateInput!): AttributeValueUpdate + attributeValueTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): AttributeValueTranslate + attributeReorderValues(attributeId: ID!, moves: [ReorderInput]!): AttributeReorderValues appCreate(input: AppInput!): AppCreate appUpdate(id: ID!, input: AppInput!): AppUpdate appDelete(id: ID!): AppDelete @@ -2761,6 +2775,11 @@ type Mutation { userAvatarUpdate(image: Upload!): UserAvatarUpdate userAvatarDelete: UserAvatarDelete userBulkSetActive(ids: [ID]!, isActive: Boolean!): UserBulkSetActive + serviceAccountCreate(input: ServiceAccountInput!): ServiceAccountCreate @deprecated(reason: "Use the `appCreate` mutation instead. This field will be removed after 2020-07-31.") + serviceAccountUpdate(id: ID!, input: ServiceAccountInput!): ServiceAccountUpdate @deprecated(reason: "Use the `appUpdate` mutation instead. This field will be removed after 2020-07-31.") + serviceAccountDelete(id: ID!): ServiceAccountDelete @deprecated(reason: "Use the `appDelete` mutation instead. This field will be removed after 2020-07-31.") + serviceAccountTokenCreate(input: ServiceAccountTokenInput!): ServiceAccountTokenCreate @deprecated(reason: "Use the `appTokenCreate` mutation instead. This field will be removed after 2020-07-31.") + serviceAccountTokenDelete(id: ID!): ServiceAccountTokenDelete @deprecated(reason: "Use the `appTokenDelete` mutation instead. This field will be removed after 2020-07-31.") permissionGroupCreate(input: PermissionGroupCreateInput!): PermissionGroupCreate permissionGroupUpdate(id: ID!, input: PermissionGroupUpdateInput!): PermissionGroupUpdate permissionGroupDelete(id: ID!): PermissionGroupDelete @@ -3149,10 +3168,24 @@ type Page implements Node & ObjectWithMetadata { publicationDate: Date isPublished: Boolean! slug: String! + pageType: PageType! created: DateTime! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! translation(languageCode: LanguageCodeEnum!): PageTranslation + attributes: [SelectedAttribute!]! +} + +type PageAttributeAssign { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageType: PageType + pageErrors: [PageError!]! +} + +type PageAttributeUnassign { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageType: PageType + pageErrors: [PageError!]! } type PageBulkDelete { @@ -3184,6 +3217,18 @@ type PageCreate { page: Page } +input PageCreateInput { + slug: String + title: String + content: String + contentJson: JSONString + attributes: [AttributeValueInput!] + isPublished: Boolean + publicationDate: String + seo: SeoInput + pageType: ID! +} + type PageDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") pageErrors: [PageError!]! @@ -3194,6 +3239,7 @@ type PageError { field: String message: String code: PageErrorCode! + attributes: [ID!] } enum PageErrorCode { @@ -3202,6 +3248,8 @@ enum PageErrorCode { NOT_FOUND REQUIRED UNIQUE + DUPLICATED_INPUT_ITEM + ATTRIBUTE_ALREADY_ASSIGNED } input PageFilterInput { @@ -3220,6 +3268,7 @@ input PageInput { title: String content: String contentJson: JSONString + attributes: [AttributeValueInput!] isPublished: Boolean publicationDate: String seo: SeoInput @@ -3273,6 +3322,85 @@ input PageTranslationInput { contentJson: JSONString } +type PageType implements Node & ObjectWithMetadata { + id: ID! + name: String! + slug: String! + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! + attributes: [Attribute] + availableAttributes(filter: AttributeFilterInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection + hasPages: Boolean +} + +type PageTypeBulkDelete { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + count: Int! + pageErrors: [PageError!]! +} + +type PageTypeCountableConnection { + pageInfo: PageInfo! + edges: [PageTypeCountableEdge!]! + totalCount: Int +} + +type PageTypeCountableEdge { + node: PageType! + cursor: String! +} + +type PageTypeCreate { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageErrors: [PageError!]! + pageType: PageType +} + +input PageTypeCreateInput { + name: String + slug: String + addAttributes: [ID!] +} + +type PageTypeDelete { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageErrors: [PageError!]! + pageType: PageType +} + +input PageTypeFilterInput { + search: String +} + +type PageTypeReorderAttributes { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageType: PageType + pageErrors: [PageError!]! +} + +enum PageTypeSortField { + NAME + SLUG +} + +input PageTypeSortingInput { + direction: OrderDirection! + field: PageTypeSortField! +} + +type PageTypeUpdate { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + pageErrors: [PageError!]! + pageType: PageType +} + +input PageTypeUpdateInput { + name: String + slug: String + addAttributes: [ID!] + removeAttributes: [ID!] +} + type PageUpdate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") pageErrors: [PageError!]! @@ -3414,6 +3542,7 @@ enum PermissionEnum { MANAGE_MENUS MANAGE_ORDERS MANAGE_PAGES + MANAGE_PAGE_TYPES_AND_ATTRIBUTES MANAGE_PRODUCTS MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES MANAGE_SHIPPING @@ -3586,6 +3715,28 @@ type Product implements Node & ObjectWithMetadata { isAvailableForPurchase: Boolean } +type ProductAttributeAssign { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + productType: ProductType + productErrors: [ProductError!]! +} + +input ProductAttributeAssignInput { + id: ID! + type: ProductAttributeType! +} + +enum ProductAttributeType { + PRODUCT + VARIANT +} + +type ProductAttributeUnassign { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + productType: ProductType + productErrors: [ProductError!]! +} + type ProductBulkDelete { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") count: Int! @@ -4150,8 +4301,6 @@ type Query { shippingZones(channel: String, before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection digitalContent(id: ID!): DigitalContent digitalContents(before: String, after: String, first: Int, last: Int): DigitalContentCountableConnection - attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection - attribute(id: ID!): Attribute categories(filter: CategoryFilterInput, sortBy: CategorySortingInput, level: Int, before: String, after: String, first: Int, last: Int): CategoryCountableConnection category(id: ID, slug: String): Category collection(id: ID, slug: String, channel: String): Collection @@ -4167,6 +4316,8 @@ type Query { payments(before: String, after: String, first: Int, last: Int): PaymentCountableConnection page(id: ID, slug: String): Page pages(sortBy: PageSortingInput, filter: PageFilterInput, before: String, after: String, first: Int, last: Int): PageCountableConnection + pageType(id: ID!): PageType + pageTypes(sortBy: PageTypeSortingInput, filter: PageTypeFilterInput, before: String, after: String, first: Int, last: Int): PageTypeCountableConnection homepageEvents(before: String, after: String, first: Int, last: Int): OrderEventCountableConnection order(id: ID!): Order orders(sortBy: OrderSortingInput, filter: OrderFilterInput, created: ReportingPeriod, status: OrderStatusFilter, channel: String, before: String, after: String, first: Int, last: Int): OrderCountableConnection @@ -4194,6 +4345,8 @@ type Query { checkoutLines(before: String, after: String, first: Int, last: Int): CheckoutLineCountableConnection channel(id: ID): Channel channels: [Channel!] + attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection + attribute(id: ID!): Attribute appsInstallations: [AppInstallation!]! apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection app(id: ID!): App @@ -4393,6 +4546,92 @@ input SeoInput { description: String } +type ServiceAccount implements Node & ObjectWithMetadata { + id: ID! + name: String + created: DateTime + isActive: Boolean + permissions: [Permission] + tokens: [ServiceAccountToken] + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! +} + +type ServiceAccountCountableConnection { + pageInfo: PageInfo! + edges: [ServiceAccountCountableEdge!]! + totalCount: Int +} + +type ServiceAccountCountableEdge { + node: ServiceAccount! + cursor: String! +} + +type ServiceAccountCreate { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + authToken: String + accountErrors: [AccountError!]! + serviceAccount: ServiceAccount +} + +type ServiceAccountDelete { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + accountErrors: [AccountError!]! + serviceAccount: ServiceAccount +} + +input ServiceAccountFilterInput { + search: String + isActive: Boolean +} + +input ServiceAccountInput { + name: String + isActive: Boolean + permissions: [PermissionEnum] +} + +enum ServiceAccountSortField { + NAME + CREATION_DATE +} + +input ServiceAccountSortingInput { + direction: OrderDirection! + field: ServiceAccountSortField! +} + +type ServiceAccountToken implements Node { + name: String + authToken: String + id: ID! +} + +type ServiceAccountTokenCreate { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + authToken: String + accountErrors: [AccountError!]! + serviceAccountToken: ServiceAccountToken +} + +type ServiceAccountTokenDelete { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + accountErrors: [AccountError!]! + serviceAccountToken: ServiceAccountToken +} + +input ServiceAccountTokenInput { + name: String + serviceAccount: ID! +} + +type ServiceAccountUpdate { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + accountErrors: [AccountError!]! + serviceAccount: ServiceAccount +} + type SetPassword { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") token: String @@ -5517,7 +5756,7 @@ enum WeightUnitsEnum { scalar _Any -union _Entity = Address | User | Group | App | ProductVariant | Product | ProductType | Collection | Category | ProductImage +union _Entity = Address | User | Group | ServiceAccount | App | ProductVariant | Product | ProductType | Collection | Category | ProductImage | PageType type _Service { sdl: String diff --git a/src/attributes/components/AttributeDetails/AttributeDetails.tsx b/src/attributes/components/AttributeDetails/AttributeDetails.tsx index 5bffdb265..73432d70b 100644 --- a/src/attributes/components/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/components/AttributeDetails/AttributeDetails.tsx @@ -5,10 +5,11 @@ import CardTitle from "@saleor/components/CardTitle"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import FormSpacer from "@saleor/components/FormSpacer"; import SingleSelectField from "@saleor/components/SingleSelectField"; -import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { commonMessages } from "@saleor/intl"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; -import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; +import { getFormErrors } from "@saleor/utils/errors"; +import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import React from "react"; import { useIntl } from "react-intl"; import slugify from "slugify"; @@ -20,7 +21,7 @@ export interface AttributeDetailsProps { canChangeType: boolean; data: AttributePageFormData; disabled: boolean; - errors: ProductErrorFragment[]; + errors: AttributeErrorFragment[]; onChange: (event: React.ChangeEvent) => void; } @@ -66,7 +67,7 @@ const AttributeDetails: React.FC = ({ })} name={"name" as keyof AttributePageFormData} fullWidth - helperText={getProductErrorMessage(formErrors.name, intl)} + helperText={getAttributeErrorMessage(formErrors.name, intl)} value={data.name} onChange={onChange} /> @@ -97,7 +98,7 @@ const AttributeDetails: React.FC = ({ choices={inputTypeChoices} disabled={disabled || !canChangeType} error={!!formErrors.inputType} - hint={getProductErrorMessage(formErrors.inputType, intl)} + hint={getAttributeErrorMessage(formErrors.inputType, intl)} label={intl.formatMessage({ defaultMessage: "Catalog Input type for Store Owner", description: "attribute's editor component" diff --git a/src/attributes/components/AttributeOrganization/AttributeOrganization.tsx b/src/attributes/components/AttributeOrganization/AttributeOrganization.tsx new file mode 100644 index 000000000..476abeec4 --- /dev/null +++ b/src/attributes/components/AttributeOrganization/AttributeOrganization.tsx @@ -0,0 +1,104 @@ +import { makeStyles } from "@material-ui/core"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import CardTitle from "@saleor/components/CardTitle"; +import RadioGroupField from "@saleor/components/RadioGroupField"; +import { AttributeTypeEnum } from "@saleor/types/globalTypes"; +import React from "react"; +import { defineMessages, FormattedMessage, useIntl } from "react-intl"; + +import { AttributePageFormData } from "../AttributePage"; + +export interface AttributeOrganizationProps { + canChangeType: boolean; + data: AttributePageFormData; + disabled: boolean; + onChange: (event: React.ChangeEvent) => void; +} + +const messages = defineMessages({ + contentAttribute: { + defaultMessage: "Content Attribute", + description: "attribute type" + }, + productAttribute: { + defaultMessage: "Product Attribute", + description: "attribute type" + } +}); + +const useStyles = makeStyles( + theme => ({ + card: { + overflow: "visible" + }, + cardSubtitle: { + fontSize: "1rem", + marginBottom: theme.spacing(0.5) + }, + label: { + marginBottom: theme.spacing(0.5) + } + }), + { name: "AttributeOrganization" } +); + +const AttributeOrganization: React.FC = props => { + const { canChangeType, data, disabled, onChange } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + return ( + + + + {canChangeType ? ( + + + + + + + } + name={"type" as keyof FormData} + value={data.type} + onChange={onChange} + /> + ) : ( + <> + + + + + {data.type === AttributeTypeEnum.PRODUCT_TYPE + ? intl.formatMessage(messages.productAttribute) + : intl.formatMessage(messages.contentAttribute)} + + + )} + + + ); +}; +AttributeOrganization.displayName = "AttributeOrganization"; +export default AttributeOrganization; diff --git a/src/attributes/components/AttributeOrganization/index.ts b/src/attributes/components/AttributeOrganization/index.ts new file mode 100644 index 000000000..61d846773 --- /dev/null +++ b/src/attributes/components/AttributeOrganization/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AttributeOrganization"; +export * from "./AttributeOrganization"; diff --git a/src/attributes/components/AttributePage/AttributePage.tsx b/src/attributes/components/AttributePage/AttributePage.tsx index 611d9bbdb..f6ca5f6de 100644 --- a/src/attributes/components/AttributePage/AttributePage.tsx +++ b/src/attributes/components/AttributePage/AttributePage.tsx @@ -12,11 +12,14 @@ import { AttributeDetailsFragment, AttributeDetailsFragment_values } from "@saleor/fragments/types/AttributeDetailsFragment"; -import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { ReorderAction } from "@saleor/types"; -import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import { + AttributeInputTypeEnum, + AttributeTypeEnum +} from "@saleor/types/globalTypes"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import React from "react"; @@ -24,13 +27,14 @@ import { useIntl } from "react-intl"; import slugify from "slugify"; import AttributeDetails from "../AttributeDetails"; +import AttributeOrganization from "../AttributeOrganization"; import AttributeProperties from "../AttributeProperties"; import AttributeValues from "../AttributeValues"; export interface AttributePageProps { attribute: AttributeDetailsFragment | null; disabled: boolean; - errors: ProductErrorFragment[]; + errors: AttributeErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; values: AttributeDetailsFragment_values[]; onBack: () => void; @@ -43,6 +47,7 @@ export interface AttributePageProps { } export interface AttributePageFormData extends MetadataFormData { + type: AttributeTypeEnum; availableInGrid: boolean; filterableInDashboard: boolean; inputType: AttributeInputTypeEnum; @@ -87,6 +92,7 @@ const AttributePage: React.FC = ({ privateMetadata: [], slug: "", storefrontSearchPosition: "", + type: AttributeTypeEnum.PRODUCT_TYPE, valueRequired: true, visibleInStorefront: true } @@ -114,6 +120,7 @@ const AttributePage: React.FC = ({ () => attribute.storefrontSearchPosition.toString(), "" ), + type: attribute?.type || AttributeTypeEnum.PRODUCT_TYPE, valueRequired: maybe(() => attribute.valueRequired, true), visibleInStorefront: maybe(() => attribute.visibleInStorefront, true) }; @@ -125,12 +132,14 @@ const AttributePage: React.FC = ({ !attribute || isPrivateMetadataModified ? data.privateMetadata : undefined; + const type = attribute === null ? data.type : undefined; return onSubmit({ ...data, metadata, privateMetadata, - slug: data.slug || slugify(data.name).toLowerCase() + slug: data.slug || slugify(data.name).toLowerCase(), + type }); }; @@ -176,6 +185,13 @@ const AttributePage: React.FC = ({
+ + ) => void; } @@ -74,42 +77,54 @@ const AttributeProperties: React.FC = ({ />
- - - {data.filterableInStorefront && ( - + {data.type === AttributeTypeEnum.PRODUCT_TYPE && ( + <> + + + )} + {data.filterableInStorefront && + data.type === AttributeTypeEnum.PRODUCT_TYPE && ( + + )} - + + + + + + } checked={data.visibleInStorefront} onChange={onChange} disabled={disabled} diff --git a/src/attributes/components/AttributeValueEditDialog/AttributeValueEditDialog.tsx b/src/attributes/components/AttributeValueEditDialog/AttributeValueEditDialog.tsx index 0eb163ce9..e39d03ad3 100644 --- a/src/attributes/components/AttributeValueEditDialog/AttributeValueEditDialog.tsx +++ b/src/attributes/components/AttributeValueEditDialog/AttributeValueEditDialog.tsx @@ -9,7 +9,7 @@ import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; -import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import { buttonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; @@ -26,7 +26,7 @@ export interface AttributeValueEditDialogProps { attributeValue: AttributeDetails_attribute_values | null; confirmButtonState: ConfirmButtonTransitionState; disabled: boolean; - errors: ProductErrorFragment[]; + errors: AttributeErrorFragment[]; open: boolean; onSubmit: (data: AttributeValueEditDialogFormData) => void; onClose: () => void; diff --git a/src/attributes/errors.ts b/src/attributes/errors.ts index a245bf8c2..f1f8a8c2b 100644 --- a/src/attributes/errors.ts +++ b/src/attributes/errors.ts @@ -1,6 +1,6 @@ -import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; -import { ProductErrorCode } from "@saleor/types/globalTypes"; -import { getProductErrorMessage } from "@saleor/utils/errors"; +import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; +import { AttributeErrorCode } from "@saleor/types/globalTypes"; +import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import { defineMessages, IntlShape } from "react-intl"; const messages = defineMessages({ @@ -13,25 +13,25 @@ const messages = defineMessages({ }); export function getAttributeSlugErrorMessage( - err: ProductErrorFragment, + err: AttributeErrorFragment, intl: IntlShape ): string { switch (err?.code) { - case ProductErrorCode.UNIQUE: + case AttributeErrorCode.UNIQUE: return intl.formatMessage(messages.attributeSlugUnique); default: - return getProductErrorMessage(err, intl); + return getAttributeErrorMessage(err, intl); } } export function getAttributeValueErrorMessage( - err: ProductErrorFragment, + err: AttributeErrorFragment, intl: IntlShape ): string { switch (err?.code) { - case ProductErrorCode.ALREADY_EXISTS: + case AttributeErrorCode.ALREADY_EXISTS: return intl.formatMessage(messages.attributeValueAlreadyExists); default: - return getProductErrorMessage(err, intl); + return getAttributeErrorMessage(err, intl); } } diff --git a/src/attributes/fixtures.ts b/src/attributes/fixtures.ts index 67788a9fe..94a6218e9 100644 --- a/src/attributes/fixtures.ts +++ b/src/attributes/fixtures.ts @@ -2,6 +2,7 @@ import { AttributeDetailsFragment } from "@saleor/fragments/types/AttributeDetai import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { AttributeInputTypeEnum, + AttributeTypeEnum, AttributeValueType } from "@saleor/types/globalTypes"; @@ -25,6 +26,7 @@ export const attribute: AttributeDetailsFragment = { privateMetadata: [], slug: "author", storefrontSearchPosition: 2, + type: AttributeTypeEnum.PRODUCT_TYPE, valueRequired: true, values: [ { @@ -55,6 +57,7 @@ export const attributes: Array(attributeBulkDelete); const attributeDelete = gql` - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeDelete($id: ID!) { attributeDelete(id: $id) { - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -68,14 +68,14 @@ export const useAttributeDeleteMutation = makeMutation< export const attributeUpdateMutation = gql` ${attributeDetailsFragment} - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeUpdate($id: ID!, $input: AttributeUpdateInput!) { attributeUpdate(id: $id, input: $input) { attribute { ...AttributeDetailsFragment } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -87,14 +87,14 @@ export const useAttributeUpdateMutation = makeMutation< const attributeValueDelete = gql` ${attributeDetailsFragment} - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeValueDelete($id: ID!) { attributeValueDelete(id: $id) { attribute { ...AttributeDetailsFragment } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -106,14 +106,14 @@ export const useAttributeValueDeleteMutation = makeMutation< export const attributeValueUpdateMutation = gql` ${attributeDetailsFragment} - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeValueUpdate($id: ID!, $input: AttributeValueCreateInput!) { attributeValueUpdate(id: $id, input: $input) { attribute { ...AttributeDetailsFragment } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -125,14 +125,14 @@ export const useAttributeValueUpdateMutation = makeMutation< export const attributeValueCreateMutation = gql` ${attributeDetailsFragment} - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeValueCreate($id: ID!, $input: AttributeValueCreateInput!) { attributeValueCreate(attribute: $id, input: $input) { attribute { ...AttributeDetailsFragment } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -144,14 +144,14 @@ export const useAttributeValueCreateMutation = makeMutation< export const attributeCreateMutation = gql` ${attributeDetailsFragment} - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeCreate($input: AttributeCreateInput!) { attributeCreate(input: $input) { attribute { ...AttributeDetailsFragment } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } @@ -162,7 +162,7 @@ export const useAttributeCreateMutation = makeMutation< >(attributeCreateMutation); const attributeValueReorderMutation = gql` - ${productErrorFragment} + ${attributeErrorFragment} mutation AttributeValueReorder($id: ID!, $move: ReorderInput!) { attributeReorderValues(attributeId: $id, moves: [$move]) { attribute { @@ -171,8 +171,8 @@ const attributeValueReorderMutation = gql` id } } - errors: productErrors { - ...ProductErrorFragment + errors: attributeErrors { + ...AttributeErrorFragment } } } diff --git a/src/attributes/types/AttributeBulkDelete.ts b/src/attributes/types/AttributeBulkDelete.ts index 259901950..590e75444 100644 --- a/src/attributes/types/AttributeBulkDelete.ts +++ b/src/attributes/types/AttributeBulkDelete.ts @@ -2,15 +2,15 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeBulkDelete // ==================================================== export interface AttributeBulkDelete_attributeBulkDelete_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeCreate.ts b/src/attributes/types/AttributeCreate.ts index 96aa56175..66d254a2a 100644 --- a/src/attributes/types/AttributeCreate.ts +++ b/src/attributes/types/AttributeCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeCreateInput, AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeCreate @@ -33,6 +33,7 @@ export interface AttributeCreate_attributeCreate_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -46,8 +47,8 @@ export interface AttributeCreate_attributeCreate_attribute { } export interface AttributeCreate_attributeCreate_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeDelete.ts b/src/attributes/types/AttributeDelete.ts index a0273fdac..5142f59e3 100644 --- a/src/attributes/types/AttributeDelete.ts +++ b/src/attributes/types/AttributeDelete.ts @@ -2,15 +2,15 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeDelete // ==================================================== export interface AttributeDelete_attributeDelete_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeDetails.ts b/src/attributes/types/AttributeDetails.ts index 1392ba7b2..6d2dab06a 100644 --- a/src/attributes/types/AttributeDetails.ts +++ b/src/attributes/types/AttributeDetails.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes"; +import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: AttributeDetails @@ -33,6 +33,7 @@ export interface AttributeDetails_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; diff --git a/src/attributes/types/AttributeList.ts b/src/attributes/types/AttributeList.ts index b63dedd6d..80ba7cd17 100644 --- a/src/attributes/types/AttributeList.ts +++ b/src/attributes/types/AttributeList.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeFilterInput, AttributeSortingInput } from "./../../types/globalTypes"; +import { AttributeFilterInput, AttributeSortingInput, AttributeTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: AttributeList @@ -20,6 +20,7 @@ export interface AttributeList_attributes_edges_node { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; diff --git a/src/attributes/types/AttributeUpdate.ts b/src/attributes/types/AttributeUpdate.ts index e06c55a1f..73c281209 100644 --- a/src/attributes/types/AttributeUpdate.ts +++ b/src/attributes/types/AttributeUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeUpdateInput, AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeUpdateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeUpdate @@ -33,6 +33,7 @@ export interface AttributeUpdate_attributeUpdate_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -46,8 +47,8 @@ export interface AttributeUpdate_attributeUpdate_attribute { } export interface AttributeUpdate_attributeUpdate_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeValueCreate.ts b/src/attributes/types/AttributeValueCreate.ts index 83861c906..afcebdde2 100644 --- a/src/attributes/types/AttributeValueCreate.ts +++ b/src/attributes/types/AttributeValueCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeValueCreate @@ -33,6 +33,7 @@ export interface AttributeValueCreate_attributeValueCreate_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -46,8 +47,8 @@ export interface AttributeValueCreate_attributeValueCreate_attribute { } export interface AttributeValueCreate_attributeValueCreate_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeValueDelete.ts b/src/attributes/types/AttributeValueDelete.ts index 4f1c4c4f1..c86dd3c74 100644 --- a/src/attributes/types/AttributeValueDelete.ts +++ b/src/attributes/types/AttributeValueDelete.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeValueDelete @@ -33,6 +33,7 @@ export interface AttributeValueDelete_attributeValueDelete_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -46,8 +47,8 @@ export interface AttributeValueDelete_attributeValueDelete_attribute { } export interface AttributeValueDelete_attributeValueDelete_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeValueReorder.ts b/src/attributes/types/AttributeValueReorder.ts index e33438c50..deaf9229d 100644 --- a/src/attributes/types/AttributeValueReorder.ts +++ b/src/attributes/types/AttributeValueReorder.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { ReorderInput, ProductErrorCode } from "./../../types/globalTypes"; +import { ReorderInput, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeValueReorder @@ -20,8 +20,8 @@ export interface AttributeValueReorder_attributeReorderValues_attribute { } export interface AttributeValueReorder_attributeReorderValues_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/types/AttributeValueUpdate.ts b/src/attributes/types/AttributeValueUpdate.ts index 5d653bf99..732b3b306 100644 --- a/src/attributes/types/AttributeValueUpdate.ts +++ b/src/attributes/types/AttributeValueUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType, AttributeErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: AttributeValueUpdate @@ -33,6 +33,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -46,8 +47,8 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute { } export interface AttributeValueUpdate_attributeValueUpdate_errors { - __typename: "ProductError"; - code: ProductErrorCode; + __typename: "AttributeError"; + code: AttributeErrorCode; field: string | null; } diff --git a/src/attributes/views/AttributeCreate/AttributeCreate.tsx b/src/attributes/views/AttributeCreate/AttributeCreate.tsx index 89a45a543..7b5634640 100644 --- a/src/attributes/views/AttributeCreate/AttributeCreate.tsx +++ b/src/attributes/views/AttributeCreate/AttributeCreate.tsx @@ -1,9 +1,9 @@ -import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { getStringOrPlaceholder } from "@saleor/misc"; import { ReorderEvent } from "@saleor/types"; -import { ProductErrorCode } from "@saleor/types/globalTypes"; +import { AttributeErrorCode } from "@saleor/types/globalTypes"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import { @@ -41,9 +41,9 @@ interface AttributeDetailsProps { params: AttributeAddUrlQueryParams; } -const attributeValueAlreadyExistsError: ProductErrorFragment = { - __typename: "ProductError", - code: ProductErrorCode.ALREADY_EXISTS, +const attributeValueAlreadyExistsError: AttributeErrorFragment = { + __typename: "AttributeError", + code: AttributeErrorCode.ALREADY_EXISTS, field: "name" }; @@ -62,9 +62,9 @@ const AttributeDetails: React.FC = ({ params }) => { const [values, setValues] = React.useState< AttributeValueEditDialogFormData[] >([]); - const [valueErrors, setValueErrors] = React.useState( - [] - ); + const [valueErrors, setValueErrors] = React.useState< + AttributeErrorFragment[] + >([]); const [attributeCreate, attributeCreateOpts] = useAttributeCreateMutation({ onCompleted: data => { diff --git a/src/attributes/views/AttributeDetails/AttributeDetails.tsx b/src/attributes/views/AttributeDetails/AttributeDetails.tsx index b6951a2ce..acfc3deb3 100644 --- a/src/attributes/views/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/views/AttributeDetails/AttributeDetails.tsx @@ -3,7 +3,7 @@ import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { ReorderEvent } from "@saleor/types"; -import { getProductErrorMessage } from "@saleor/utils/errors"; +import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import { move } from "@saleor/utils/lists"; @@ -140,7 +140,7 @@ const AttributeDetails: React.FC = ({ id, params }) => { if (data.attributeReorderValues.errors.length !== 0) { notify({ status: "error", - text: getProductErrorMessage( + text: getAttributeErrorMessage( data.attributeReorderValues.errors[0], intl ) diff --git a/src/storybook/stories/components/AssignAttributeDialog.tsx b/src/components/AssignAttributeDialog/AssignAttributeDialog.stories.tsx similarity index 90% rename from src/storybook/stories/components/AssignAttributeDialog.tsx rename to src/components/AssignAttributeDialog/AssignAttributeDialog.stories.tsx index e721abc00..822e754fc 100644 --- a/src/storybook/stories/components/AssignAttributeDialog.tsx +++ b/src/components/AssignAttributeDialog/AssignAttributeDialog.stories.tsx @@ -1,13 +1,13 @@ import { attributes } from "@saleor/attributes/fixtures"; -import { fetchMoreProps } from "@saleor/fixtures"; import AssignAttributeDialog, { AssignAttributeDialogProps -} from "@saleor/productTypes/components/AssignAttributeDialog"; +} from "@saleor/components/AssignAttributeDialog"; +import { fetchMoreProps } from "@saleor/fixtures"; import { formError } from "@saleor/storybook/misc"; import { storiesOf } from "@storybook/react"; import React from "react"; -import Decorator from "../../Decorator"; +import Decorator from "../../storybook/Decorator"; const props: AssignAttributeDialogProps = { ...fetchMoreProps, diff --git a/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx b/src/components/AssignAttributeDialog/AssignAttributeDialog.tsx similarity index 96% rename from src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx rename to src/components/AssignAttributeDialog/AssignAttributeDialog.tsx index 32040d27b..b484d64c6 100644 --- a/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx +++ b/src/components/AssignAttributeDialog/AssignAttributeDialog.tsx @@ -16,6 +16,7 @@ import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; +import { AvailableAttributeFragment } from "@saleor/fragments/types/AvailableAttributeFragment"; import useElementScroll, { isScrolledToBottom } from "@saleor/hooks/useElementScroll"; @@ -30,8 +31,6 @@ import React from "react"; import InfiniteScroll from "react-infinite-scroller"; import { FormattedMessage, useIntl } from "react-intl"; -import { SearchAvailableAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes"; - const useStyles = makeStyles( theme => ({ actions: { @@ -63,7 +62,7 @@ export interface AssignAttributeDialogProps extends FetchMoreProps { confirmButtonState: ConfirmButtonTransitionState; errors: string[]; open: boolean; - attributes: SearchAvailableAttributes_productType_availableAttributes_edges_node[]; + attributes: AvailableAttributeFragment[]; selected: string[]; onClose: () => void; onFetch: (query: string) => void; diff --git a/src/productTypes/components/AssignAttributeDialog/index.ts b/src/components/AssignAttributeDialog/index.ts similarity index 100% rename from src/productTypes/components/AssignAttributeDialog/index.ts rename to src/components/AssignAttributeDialog/index.ts diff --git a/src/components/AttributeUnassignDialog/AttributeUnassignDialog.stories.tsx b/src/components/AttributeUnassignDialog/AttributeUnassignDialog.stories.tsx new file mode 100644 index 000000000..8e740477e --- /dev/null +++ b/src/components/AttributeUnassignDialog/AttributeUnassignDialog.stories.tsx @@ -0,0 +1,21 @@ +import AttributeUnassignDialog, { + AttributeUnassignDialogProps +} from "@saleor/components/AttributeUnassignDialog"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "../../storybook/Decorator"; + +const props: AttributeUnassignDialogProps = { + attributeName: "Size", + confirmButtonState: "default", + itemTypeName: "Shoes", + onClose: () => undefined, + onConfirm: () => undefined, + open: true, + title: "Unassign Attribute from Shoes" +}; + +storiesOf("Generics / Unassign attribute", module) + .addDecorator(Decorator) + .add("default", () => ); diff --git a/src/components/AttributeUnassignDialog/AttributeUnassignDialog.tsx b/src/components/AttributeUnassignDialog/AttributeUnassignDialog.tsx new file mode 100644 index 000000000..1d552fe29 --- /dev/null +++ b/src/components/AttributeUnassignDialog/AttributeUnassignDialog.tsx @@ -0,0 +1,45 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import ActionDialog from "@saleor/components/ActionDialog"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +export interface AttributeUnassignDialogProps { + title: string; + attributeName: string; + confirmButtonState: ConfirmButtonTransitionState; + open: boolean; + itemTypeName: string; + onClose: () => void; + onConfirm: () => void; +} + +const AttributeUnassignDialog: React.FC = ({ + title, + attributeName, + confirmButtonState, + open, + itemTypeName, + onClose, + onConfirm +}) => ( + + + {attributeName}, + itemTypeName: {itemTypeName} + }} + /> + + +); +AttributeUnassignDialog.displayName = "AttributeUnassignDialog"; +export default AttributeUnassignDialog; diff --git a/src/components/AttributeUnassignDialog/index.ts b/src/components/AttributeUnassignDialog/index.ts new file mode 100644 index 000000000..3723e6059 --- /dev/null +++ b/src/components/AttributeUnassignDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AttributeUnassignDialog"; +export * from "./AttributeUnassignDialog"; diff --git a/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.stories.tsx b/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.stories.tsx new file mode 100644 index 000000000..d69c74d28 --- /dev/null +++ b/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.stories.tsx @@ -0,0 +1,21 @@ +import BulkAttributeUnassignDialog, { + BulkAttributeUnassignDialogProps +} from "@saleor/components/BulkAttributeUnassignDialog"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "../../storybook/Decorator"; + +const props: BulkAttributeUnassignDialogProps = { + attributeQuantity: 4, + confirmButtonState: "default", + itemTypeName: "Shoes", + onClose: () => undefined, + onConfirm: () => undefined, + open: true, + title: "Unassign Attribute from Shoes" +}; + +storiesOf("Generics / Unassign multiple attributes", module) + .addDecorator(Decorator) + .add("default", () => ); diff --git a/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.tsx b/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.tsx new file mode 100644 index 000000000..18da355b7 --- /dev/null +++ b/src/components/BulkAttributeUnassignDialog/BulkAttributeUnassignDialog.tsx @@ -0,0 +1,47 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import ActionDialog from "@saleor/components/ActionDialog"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +export interface BulkAttributeUnassignDialogProps { + title: string; + attributeQuantity: number; + confirmButtonState: ConfirmButtonTransitionState; + open: boolean; + itemTypeName: string; + onClose: () => void; + onConfirm: () => void; +} + +const BulkAttributeUnassignDialog: React.FC = ({ + title, + attributeQuantity, + confirmButtonState, + open, + itemTypeName, + onClose, + onConfirm +}) => ( + + + {attributeQuantity}, + counter: attributeQuantity, + itemTypeName: {itemTypeName} + }} + /> + + +); +BulkAttributeUnassignDialog.displayName = "BulkAttributeUnassignDialog"; +export default BulkAttributeUnassignDialog; diff --git a/src/components/BulkAttributeUnassignDialog/index.ts b/src/components/BulkAttributeUnassignDialog/index.ts new file mode 100644 index 000000000..5bb131e8d --- /dev/null +++ b/src/components/BulkAttributeUnassignDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./BulkAttributeUnassignDialog"; +export * from "./BulkAttributeUnassignDialog"; diff --git a/src/components/RadioGroupField/RadioGroupField.tsx b/src/components/RadioGroupField/RadioGroupField.tsx index 2ef01e5a7..fc46cca80 100644 --- a/src/components/RadioGroupField/RadioGroupField.tsx +++ b/src/components/RadioGroupField/RadioGroupField.tsx @@ -50,7 +50,7 @@ interface RadioGroupFieldProps { disabled?: boolean; error?: boolean; hint?: string; - label?: string; + label?: React.ReactNode; name?: string; value: string | number; onChange: (event: React.ChangeEvent) => void; diff --git a/src/configuration/index.tsx b/src/configuration/index.tsx index 451c1feae..fd5f9d41f 100644 --- a/src/configuration/index.tsx +++ b/src/configuration/index.tsx @@ -7,6 +7,7 @@ import Attributes from "@saleor/icons/Attributes"; import Channels from "@saleor/icons/Channels"; import Navigation from "@saleor/icons/Navigation"; import Pages from "@saleor/icons/Pages"; +import PageTypes from "@saleor/icons/PageTypes"; import PermissionGroups from "@saleor/icons/PermissionGroups"; import Plugins from "@saleor/icons/Plugins"; import ProductTypes from "@saleor/icons/ProductTypes"; @@ -19,6 +20,7 @@ import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { menuListUrl } from "@saleor/navigation/urls"; import { pageListUrl } from "@saleor/pages/urls"; +import { pageTypeListUrl } from "@saleor/pageTypes/urls"; import { permissionGroupListUrl } from "@saleor/permissionGroups/urls"; import { pluginListUrl } from "@saleor/plugins/urls"; import { productTypeListUrl } from "@saleor/productTypes/urls"; @@ -151,6 +153,33 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] { } ] }, + { + label: intl.formatMessage({ + defaultMessage: "Content Management" + }), + menuItems: [ + { + description: intl.formatMessage({ + defaultMessage: "Define types of content pages used in your store", + id: "configurationMenuPageTypes" + }), + icon: , + permission: PermissionEnum.MANAGE_PAGES, + title: intl.formatMessage(sectionNames.pageTypes), + url: pageTypeListUrl() + }, + { + description: intl.formatMessage({ + defaultMessage: "Manage and add additional pages", + id: "configurationMenuPages" + }), + icon: , + permission: PermissionEnum.MANAGE_PAGES, + title: intl.formatMessage(sectionNames.pages), + url: pageListUrl() + } + ] + }, { label: intl.formatMessage({ defaultMessage: "Miscellaneous" @@ -176,16 +205,6 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] { title: intl.formatMessage(sectionNames.siteSettings), url: siteSettingsUrl() }, - { - description: intl.formatMessage({ - defaultMessage: "Manage and add additional pages", - id: "configurationMenuPages" - }), - icon: , - permission: PermissionEnum.MANAGE_PAGES, - title: intl.formatMessage(sectionNames.pages), - url: pageListUrl() - }, { description: intl.formatMessage({ defaultMessage: "View and update your plugins and their settings.", diff --git a/src/fragments/attributes.ts b/src/fragments/attributes.ts index 66332cc49..ae509736f 100644 --- a/src/fragments/attributes.ts +++ b/src/fragments/attributes.ts @@ -7,6 +7,7 @@ export const attributeFragment = gql` id name slug + type visibleInStorefront filterableInDashboard filterableInStorefront @@ -31,3 +32,11 @@ export const attributeDetailsFragment = gql` } } `; + +export const availableAttributeFragment = gql` + fragment AvailableAttributeFragment on Attribute { + id + name + slug + } +`; diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index 26d79d844..92dbdfa80 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -1,5 +1,12 @@ import gql from "graphql-tag"; +export const attributeErrorFragment = gql` + fragment AttributeErrorFragment on AttributeError { + code + field + } +`; + export const productErrorFragment = gql` fragment ProductErrorFragment on ProductError { code @@ -69,6 +76,14 @@ export const pageErrorFragment = gql` } `; +export const pageErrorWithAttributesFragment = gql` + ${pageErrorFragment} + fragment PageErrorWithAttributesFragment on PageError { + ...PageErrorFragment + attributes + } +`; + export const permissionGroupErrorFragment = gql` fragment PermissionGroupErrorFragment on PermissionGroupError { code diff --git a/src/fragments/pageTypes.ts b/src/fragments/pageTypes.ts new file mode 100644 index 000000000..d87c1cba9 --- /dev/null +++ b/src/fragments/pageTypes.ts @@ -0,0 +1,24 @@ +import gql from "graphql-tag"; + +import { attributeFragment } from "./attributes"; +import { metadataFragment } from "./metadata"; + +export const pageTypeFragment = gql` + fragment PageTypeFragment on PageType { + id + name + } +`; + +export const pageTypeDetailsFragment = gql` + ${attributeFragment} + ${pageTypeFragment} + ${metadataFragment} + fragment PageTypeDetailsFragment on PageType { + ...PageTypeFragment + ...MetadataFragment + attributes { + ...AttributeFragment + } + } +`; diff --git a/src/fragments/pages.ts b/src/fragments/pages.ts index ad6cb6b4a..8ee33939a 100644 --- a/src/fragments/pages.ts +++ b/src/fragments/pages.ts @@ -11,11 +11,52 @@ export const pageFragment = gql` } `; +export const pageAttributesFragment = gql` + fragment PageAttributesFragment on Page { + attributes { + attribute { + id + slug + name + inputType + valueRequired + values { + id + name + slug + } + } + values { + id + name + slug + } + } + pageType { + id + name + attributes { + id + name + inputType + valueRequired + values { + id + name + slug + } + } + } + } +`; + export const pageDetailsFragment = gql` ${pageFragment} + ${pageAttributesFragment} ${metadataFragment} fragment PageDetailsFragment on Page { ...PageFragment + ...PageAttributesFragment ...MetadataFragment contentJson seoTitle diff --git a/src/fragments/types/AttributeDetailsFragment.ts b/src/fragments/types/AttributeDetailsFragment.ts index 6d9faedd0..665617d63 100644 --- a/src/fragments/types/AttributeDetailsFragment.ts +++ b/src/fragments/types/AttributeDetailsFragment.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes"; +import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes"; // ==================================================== // GraphQL fragment: AttributeDetailsFragment @@ -33,6 +33,7 @@ export interface AttributeDetailsFragment { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; diff --git a/src/fragments/types/AttributeErrorFragment.ts b/src/fragments/types/AttributeErrorFragment.ts new file mode 100644 index 000000000..f16ef08c3 --- /dev/null +++ b/src/fragments/types/AttributeErrorFragment.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { AttributeErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: AttributeErrorFragment +// ==================================================== + +export interface AttributeErrorFragment { + __typename: "AttributeError"; + code: AttributeErrorCode; + field: string | null; +} diff --git a/src/fragments/types/AttributeFragment.ts b/src/fragments/types/AttributeFragment.ts index 333736761..94d9e07b7 100644 --- a/src/fragments/types/AttributeFragment.ts +++ b/src/fragments/types/AttributeFragment.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL fragment: AttributeFragment // ==================================================== @@ -11,6 +13,7 @@ export interface AttributeFragment { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; diff --git a/src/fragments/types/AvailableAttributeFragment.ts b/src/fragments/types/AvailableAttributeFragment.ts new file mode 100644 index 000000000..40a51a4c4 --- /dev/null +++ b/src/fragments/types/AvailableAttributeFragment.ts @@ -0,0 +1,14 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: AvailableAttributeFragment +// ==================================================== + +export interface AvailableAttributeFragment { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; +} diff --git a/src/fragments/types/MetadataFragment.ts b/src/fragments/types/MetadataFragment.ts index bca054ef9..fe445c9b6 100644 --- a/src/fragments/types/MetadataFragment.ts +++ b/src/fragments/types/MetadataFragment.ts @@ -19,7 +19,7 @@ export interface MetadataFragment_privateMetadata { } export interface MetadataFragment { - __typename: "App" | "ShippingZone" | "ShippingMethod" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "Page" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice"; + __typename: "ServiceAccount" | "App" | "ShippingZone" | "ShippingMethod" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "Page" | "PageType" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice"; metadata: (MetadataFragment_metadata | null)[]; privateMetadata: (MetadataFragment_privateMetadata | null)[]; } diff --git a/src/fragments/types/PageAttributesFragment.ts b/src/fragments/types/PageAttributesFragment.ts new file mode 100644 index 000000000..244eb87db --- /dev/null +++ b/src/fragments/types/PageAttributesFragment.ts @@ -0,0 +1,68 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: PageAttributesFragment +// ==================================================== + +export interface PageAttributesFragment_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageAttributesFragment_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageAttributesFragment_attributes_attribute_values | null)[] | null; +} + +export interface PageAttributesFragment_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageAttributesFragment_attributes { + __typename: "SelectedAttribute"; + attribute: PageAttributesFragment_attributes_attribute; + values: (PageAttributesFragment_attributes_values | null)[]; +} + +export interface PageAttributesFragment_pageType_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageAttributesFragment_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageAttributesFragment_pageType_attributes_values | null)[] | null; +} + +export interface PageAttributesFragment_pageType { + __typename: "PageType"; + id: string; + name: string; + attributes: (PageAttributesFragment_pageType_attributes | null)[] | null; +} + +export interface PageAttributesFragment { + __typename: "Page"; + attributes: PageAttributesFragment_attributes[]; + pageType: PageAttributesFragment_pageType; +} diff --git a/src/fragments/types/PageDetailsFragment.ts b/src/fragments/types/PageDetailsFragment.ts index a6f606772..fc69b5871 100644 --- a/src/fragments/types/PageDetailsFragment.ts +++ b/src/fragments/types/PageDetailsFragment.ts @@ -2,10 +2,65 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL fragment: PageDetailsFragment // ==================================================== +export interface PageDetailsFragment_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetailsFragment_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageDetailsFragment_attributes_attribute_values | null)[] | null; +} + +export interface PageDetailsFragment_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetailsFragment_attributes { + __typename: "SelectedAttribute"; + attribute: PageDetailsFragment_attributes_attribute; + values: (PageDetailsFragment_attributes_values | null)[]; +} + +export interface PageDetailsFragment_pageType_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetailsFragment_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageDetailsFragment_pageType_attributes_values | null)[] | null; +} + +export interface PageDetailsFragment_pageType { + __typename: "PageType"; + id: string; + name: string; + attributes: (PageDetailsFragment_pageType_attributes | null)[] | null; +} + export interface PageDetailsFragment_metadata { __typename: "MetadataItem"; key: string; @@ -24,6 +79,8 @@ export interface PageDetailsFragment { title: string; slug: string; isPublished: boolean; + attributes: PageDetailsFragment_attributes[]; + pageType: PageDetailsFragment_pageType; metadata: (PageDetailsFragment_metadata | null)[]; privateMetadata: (PageDetailsFragment_privateMetadata | null)[]; contentJson: any; diff --git a/src/fragments/types/PageErrorWithAttributesFragment.ts b/src/fragments/types/PageErrorWithAttributesFragment.ts new file mode 100644 index 000000000..5b2c41ae5 --- /dev/null +++ b/src/fragments/types/PageErrorWithAttributesFragment.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: PageErrorWithAttributesFragment +// ==================================================== + +export interface PageErrorWithAttributesFragment { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; + attributes: string[] | null; +} diff --git a/src/fragments/types/PageTypeDetailsFragment.ts b/src/fragments/types/PageTypeDetailsFragment.ts new file mode 100644 index 000000000..dc0718772 --- /dev/null +++ b/src/fragments/types/PageTypeDetailsFragment.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: PageTypeDetailsFragment +// ==================================================== + +export interface PageTypeDetailsFragment_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeDetailsFragment_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeDetailsFragment_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface PageTypeDetailsFragment { + __typename: "PageType"; + id: string; + name: string; + metadata: (PageTypeDetailsFragment_metadata | null)[]; + privateMetadata: (PageTypeDetailsFragment_privateMetadata | null)[]; + attributes: (PageTypeDetailsFragment_attributes | null)[] | null; +} diff --git a/src/fragments/types/PageTypeFragment.ts b/src/fragments/types/PageTypeFragment.ts new file mode 100644 index 000000000..4c134c030 --- /dev/null +++ b/src/fragments/types/PageTypeFragment.ts @@ -0,0 +1,13 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: PageTypeFragment +// ==================================================== + +export interface PageTypeFragment { + __typename: "PageType"; + id: string; + name: string; +} diff --git a/src/fragments/types/ProductTypeDetailsFragment.ts b/src/fragments/types/ProductTypeDetailsFragment.ts index 3c9851cd3..413b65998 100644 --- a/src/fragments/types/ProductTypeDetailsFragment.ts +++ b/src/fragments/types/ProductTypeDetailsFragment.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { WeightUnitsEnum } from "./../../types/globalTypes"; +import { AttributeTypeEnum, WeightUnitsEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL fragment: ProductTypeDetailsFragment @@ -31,6 +31,7 @@ export interface ProductTypeDetailsFragment_productAttributes { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; @@ -41,6 +42,7 @@ export interface ProductTypeDetailsFragment_variantAttributes { id: string; name: string | null; slug: string | null; + type: AttributeTypeEnum | null; visibleInStorefront: boolean; filterableInDashboard: boolean; filterableInStorefront: boolean; diff --git a/src/icons/PageTypes.tsx b/src/icons/PageTypes.tsx new file mode 100644 index 000000000..59f363caa --- /dev/null +++ b/src/icons/PageTypes.tsx @@ -0,0 +1,16 @@ +import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; +import React from "react"; + +const PageTypes = createSvgIcon( + <> + + , + "PageTypes" +); + +export default PageTypes; diff --git a/src/index.tsx b/src/index.tsx index fb59f83e8..99fb7e065 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -46,6 +46,7 @@ import { navigationSection } from "./navigation/urls"; import { NotFound } from "./NotFound"; import OrdersSection from "./orders"; import PageSection from "./pages"; +import PageTypesSection from "./pageTypes"; import PermissionGroupSection from "./permissionGroups"; import PluginsSection from "./plugins"; import ProductSection from "./products"; @@ -177,6 +178,11 @@ const Routes: React.FC = () => { path="/pages" component={PageSection} /> + void; + onAttributeClick: (id: string) => void; + onAttributeReorder: ReorderAction; + onAttributeUnassign: (id: string) => void; +} + +const numberOfColumns = 5; + +const PageTypeAttributes: React.FC = props => { + const { + attributes, + disabled, + isChecked, + selected, + toggle, + toggleAll, + toolbar, + type, + onAttributeAssign, + onAttributeClick, + onAttributeReorder, + onAttributeUnassign + } = props; + const classes = useStyles(props); + + const intl = useIntl(); + + return ( + + onAttributeAssign(AttributeTypeEnum[type])} + > + + + } + /> + + + + + + + + + {attributes?.length > 0 && ( + + + + + + + + + + )} + + {renderCollection( + attributes, + (attribute, attributeIndex) => { + const isSelected = attribute ? isChecked(attribute.id) : false; + + return ( + onAttributeClick(attribute.id) + : undefined + } + key={attribute?.id} + index={attributeIndex || 0} + data-test="id" + data-test-id={attribute?.id} + > + + toggle(attribute.id)} + /> + + + {attribute?.name || } + + + {attribute?.slug || } + + + + onAttributeUnassign(attribute.id) + )} + > + + + + + ); + }, + () => ( + + + + + + ) + )} + + + + ); +}; +PageTypeAttributes.displayName = "PageTypeAttributes"; +export default PageTypeAttributes; diff --git a/src/pageTypes/components/PageTypeAttributes/index.ts b/src/pageTypes/components/PageTypeAttributes/index.ts new file mode 100644 index 000000000..2bfb2962a --- /dev/null +++ b/src/pageTypes/components/PageTypeAttributes/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeAttributes"; +export * from "./PageTypeAttributes"; diff --git a/src/pageTypes/components/PageTypeBulkDeleteDialog/PageTypeBulkDeleteDialog.tsx b/src/pageTypes/components/PageTypeBulkDeleteDialog/PageTypeBulkDeleteDialog.tsx new file mode 100644 index 000000000..34635bae0 --- /dev/null +++ b/src/pageTypes/components/PageTypeBulkDeleteDialog/PageTypeBulkDeleteDialog.tsx @@ -0,0 +1,63 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import ActionDialog from "@saleor/components/ActionDialog"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +export interface PageTypeBulkDeleteDialogProps { + confirmButtonState: ConfirmButtonTransitionState; + open: boolean; + quantity: number; + hasPages: boolean; + onClose: () => void; + onConfirm: () => void; +} + +const PageTypeBulkDeleteDialog: React.FC = ({ + confirmButtonState, + open, + quantity, + hasPages, + onClose, + onConfirm +}) => { + const intl = useIntl(); + + return ( + + + {hasPages ? ( + {quantity} + }} + /> + ) : ( + {quantity} + }} + /> + )} + + + ); +}; +PageTypeBulkDeleteDialog.displayName = "PageTypeBulkDeleteDialog"; +export default PageTypeBulkDeleteDialog; diff --git a/src/pageTypes/components/PageTypeBulkDeleteDialog/index.ts b/src/pageTypes/components/PageTypeBulkDeleteDialog/index.ts new file mode 100644 index 000000000..883b5d2bc --- /dev/null +++ b/src/pageTypes/components/PageTypeBulkDeleteDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeBulkDeleteDialog"; +export * from "./PageTypeBulkDeleteDialog"; diff --git a/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.stories.tsx b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.stories.tsx new file mode 100644 index 000000000..528b63c21 --- /dev/null +++ b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.stories.tsx @@ -0,0 +1,34 @@ +import { Omit } from "@material-ui/core"; +import { PageErrorCode } from "@saleor/types/globalTypes"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "../../../storybook/Decorator"; +import PageTypeCreatePage, { PageTypeCreatePageProps } from "."; + +const props: Omit = { + disabled: false, + errors: [], + onBack: () => undefined, + onSubmit: () => undefined, + saveButtonBarState: "default" +}; + +storiesOf("Views / Page types / Create page type", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ) + .add("form errors", () => ( + ({ + __typename: "PageError", + ...err + }))} + /> + )); diff --git a/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx new file mode 100644 index 000000000..3b3f7b2a7 --- /dev/null +++ b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx @@ -0,0 +1,114 @@ +import { makeStyles } from "@material-ui/core"; +import Typography from "@material-ui/core/Typography"; +import AppHeader from "@saleor/components/AppHeader"; +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 Hr from "@saleor/components/Hr"; +import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; +import PageHeader from "@saleor/components/PageHeader"; +import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { commonMessages, sectionNames } from "@saleor/intl"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import PageTypeDetails from "../PageTypeDetails/PageTypeDetails"; + +export interface PageTypeForm extends MetadataFormData { + name: string; +} + +export interface PageTypeCreatePageProps { + errors: PageErrorFragment[]; + disabled: boolean; + saveButtonBarState: ConfirmButtonTransitionState; + onBack: () => void; + onSubmit: (data: PageTypeForm) => void; +} + +const formInitialData: PageTypeForm = { + metadata: [], + name: "", + privateMetadata: [] +}; + +const useStyles = makeStyles( + theme => ({ + hr: { + gridColumnEnd: "span 2", + margin: theme.spacing(1, 0) + } + }), + { + name: "PageTypeCreatePage" + } +); + +const PageTypeCreatePage: React.FC = props => { + const { disabled, errors, saveButtonBarState, onBack, onSubmit } = props; + const classes = useStyles(props); + const intl = useIntl(); + const { + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + return ( +
+ {({ change, data, hasChanged, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.pageTypes)} + + + +
+ + {intl.formatMessage(commonMessages.generalInformations)} + + + + +
+ +
+
+ + + +
+ +
+
+ +
+ ); + }} +
+ ); +}; +PageTypeCreatePage.displayName = "PageTypeCreatePage"; +export default PageTypeCreatePage; diff --git a/src/pageTypes/components/PageTypeCreatePage/index.ts b/src/pageTypes/components/PageTypeCreatePage/index.ts new file mode 100644 index 000000000..a38142a2e --- /dev/null +++ b/src/pageTypes/components/PageTypeCreatePage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeCreatePage"; +export * from "./PageTypeCreatePage"; diff --git a/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx b/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx new file mode 100644 index 000000000..8cef40620 --- /dev/null +++ b/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx @@ -0,0 +1,61 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import ActionDialog from "@saleor/components/ActionDialog"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +export interface PageTypeDeleteDialogProps { + confirmButtonState: ConfirmButtonTransitionState; + open: boolean; + name: string; + hasPages: boolean; + onClose: () => void; + onConfirm: () => void; +} + +const PageTypeDeleteDialog: React.FC = ({ + confirmButtonState, + open, + name, + hasPages, + onClose, + onConfirm +}) => { + const intl = useIntl(); + + return ( + + + {hasPages ? ( + {name} + }} + /> + ) : ( + {name} + }} + /> + )} + + + ); +}; +PageTypeDeleteDialog.displayName = "PageTypeDeleteDialog"; +export default PageTypeDeleteDialog; diff --git a/src/pageTypes/components/PageTypeDeleteDialog/index.ts b/src/pageTypes/components/PageTypeDeleteDialog/index.ts new file mode 100644 index 000000000..24fe8502b --- /dev/null +++ b/src/pageTypes/components/PageTypeDeleteDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeDeleteDialog"; +export * from "./PageTypeDeleteDialog"; diff --git a/src/pageTypes/components/PageTypeDetails/PageTypeDetails.tsx b/src/pageTypes/components/PageTypeDetails/PageTypeDetails.tsx new file mode 100644 index 000000000..b1a6bc32c --- /dev/null +++ b/src/pageTypes/components/PageTypeDetails/PageTypeDetails.tsx @@ -0,0 +1,53 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import TextField from "@material-ui/core/TextField"; +import CardTitle from "@saleor/components/CardTitle"; +import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { commonMessages } from "@saleor/intl"; +import { getFormErrors } from "@saleor/utils/errors"; +import getPageErrorMessage from "@saleor/utils/errors/page"; +import React from "react"; +import { useIntl } from "react-intl"; + +interface PageTypeDetailsProps { + data?: { + name: string; + }; + disabled: boolean; + errors: PageErrorFragment[]; + onChange: (event: React.ChangeEvent) => void; +} + +const PageTypeDetails: React.FC = props => { + const { data, disabled, errors, onChange } = props; + const intl = useIntl(); + + const formErrors = getFormErrors(["name"], errors); + + return ( + + + + + + + ); +}; +PageTypeDetails.defaultProps = { + errors: [] +}; +PageTypeDetails.displayName = "PageTypeDetails"; +export default PageTypeDetails; diff --git a/src/pageTypes/components/PageTypeDetails/index.ts b/src/pageTypes/components/PageTypeDetails/index.ts new file mode 100644 index 000000000..50323ee37 --- /dev/null +++ b/src/pageTypes/components/PageTypeDetails/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeDetails"; +export * from "./PageTypeDetails"; diff --git a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.stories.tsx b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.stories.tsx new file mode 100644 index 000000000..828c4d38f --- /dev/null +++ b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.stories.tsx @@ -0,0 +1,60 @@ +import { Omit } from "@material-ui/core"; +import { listActionsProps } from "@saleor/fixtures"; +import { PageErrorCode } from "@saleor/types/globalTypes"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "../../../storybook/Decorator"; +import { pageType } from "../../fixtures"; +import PageTypeDetailsPage, { PageTypeDetailsPageProps } from "."; + +const props: Omit = { + attributeList: listActionsProps, + disabled: false, + errors: [], + onAttributeAdd: () => undefined, + onAttributeClick: () => undefined, + onAttributeReorder: () => undefined, + onAttributeUnassign: () => undefined, + onBack: () => undefined, + onDelete: () => undefined, + onSubmit: () => undefined, + pageTitle: pageType.name, + pageType, + saveButtonBarState: "default" +}; + +storiesOf("Views / Page types / Page type details", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ( + + )) + .add("no attributes", () => ( + + )) + .add("form errors", () => ( + ({ + __typename: "PageError", + ...err + }))} + /> + )); diff --git a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx new file mode 100644 index 000000000..70b390c4c --- /dev/null +++ b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx @@ -0,0 +1,183 @@ +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import AppHeader from "@saleor/components/AppHeader"; +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 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 { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; +import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { commonMessages, sectionNames } from "@saleor/intl"; +import { PageTypeDetails_pageType } from "@saleor/pageTypes/types/PageTypeDetails"; +import { ListActions, ReorderEvent } from "@saleor/types"; +import { AttributeTypeEnum } from "@saleor/types/globalTypes"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import PageTypeAttributes from "../PageTypeAttributes/PageTypeAttributes"; +import PageTypeDetails from "../PageTypeDetails/PageTypeDetails"; + +export interface PageTypeForm extends MetadataFormData { + name: string; + attributes: SingleAutocompleteChoiceType[]; +} + +export interface PageTypeDetailsPageProps { + errors: PageErrorFragment[]; + pageType: PageTypeDetails_pageType; + disabled: boolean; + pageTitle: string; + attributeList: ListActions; + saveButtonBarState: ConfirmButtonTransitionState; + onAttributeAdd: (type: AttributeTypeEnum) => void; + onAttributeClick: (id: string) => void; + onAttributeReorder: (event: ReorderEvent, type: AttributeTypeEnum) => void; + onAttributeUnassign: (id: string) => void; + onBack: () => void; + onDelete: () => void; + onSubmit: (data: PageTypeForm) => void; +} + +const useStyles = makeStyles( + theme => ({ + hr: { + gridColumnEnd: "span 2", + margin: theme.spacing(1, 0) + } + }), + { + name: "PageTypeDetailsPage" + } +); + +const PageTypeDetailsPage: React.FC = props => { + const { + disabled, + errors, + pageTitle, + pageType, + attributeList, + saveButtonBarState, + onAttributeAdd, + onAttributeUnassign, + onAttributeReorder, + onAttributeClick, + onBack, + onDelete, + onSubmit + } = props; + const classes = useStyles(props); + const intl = useIntl(); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const formInitialData: PageTypeForm = { + attributes: + pageType?.attributes?.map(attribute => ({ + label: attribute.name, + value: attribute.id + })) || [], + metadata: pageType?.metadata?.map(mapMetadataItemToInput), + name: pageType?.name || "", + privateMetadata: pageType?.privateMetadata?.map(mapMetadataItemToInput) + }; + + const handleSubmit = (data: PageTypeForm) => { + const metadata = isMetadataModified ? data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? data.privateMetadata + : undefined; + + onSubmit({ + ...data, + metadata, + privateMetadata + }); + }; + + return ( +
+ {({ change, data, hasChanged, submit }) => { + const changeMetadata = makeMetadataChangeHandler(change); + + return ( + + + {intl.formatMessage(sectionNames.pageTypes)} + + + +
+ + {intl.formatMessage(commonMessages.generalInformations)} + + + + +
+ +
+
+ + + + + + +
+ + onAttributeReorder(event, AttributeTypeEnum.PAGE_TYPE) + } + onAttributeUnassign={onAttributeUnassign} + {...attributeList} + /> +
+
+ + + +
+ +
+ +
+ ); + }} +
+ ); +}; +PageTypeDetailsPage.displayName = "PageTypeDetailsPage"; +export default PageTypeDetailsPage; diff --git a/src/pageTypes/components/PageTypeDetailsPage/index.ts b/src/pageTypes/components/PageTypeDetailsPage/index.ts new file mode 100644 index 000000000..b28e29b0f --- /dev/null +++ b/src/pageTypes/components/PageTypeDetailsPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeDetailsPage"; +export * from "./PageTypeDetailsPage"; diff --git a/src/pageTypes/components/PageTypeList/PageTypeList.tsx b/src/pageTypes/components/PageTypeList/PageTypeList.tsx new file mode 100644 index 000000000..79aac2d40 --- /dev/null +++ b/src/pageTypes/components/PageTypeList/PageTypeList.tsx @@ -0,0 +1,145 @@ +import { makeStyles } from "@material-ui/core/styles"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableFooter from "@material-ui/core/TableFooter"; +import TableRow from "@material-ui/core/TableRow"; +import Checkbox from "@saleor/components/Checkbox"; +import ResponsiveTable from "@saleor/components/ResponsiveTable"; +import Skeleton from "@saleor/components/Skeleton"; +import TableCellHeader from "@saleor/components/TableCellHeader"; +import TableHead from "@saleor/components/TableHead"; +import TablePagination from "@saleor/components/TablePagination"; +import { PageTypeList_pageTypes_edges_node } from "@saleor/pageTypes/types/PageTypeList"; +import { PageTypeListUrlSortField } from "@saleor/pageTypes/urls"; +import { getArrowDirection } from "@saleor/utils/sort"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { renderCollection } from "../../../misc"; +import { ListActions, ListProps, SortPage } from "../../../types"; + +const useStyles = makeStyles( + { + colName: { + paddingLeft: 0 + }, + link: { + cursor: "pointer" + } + }, + { name: "PageTypeList" } +); + +interface PageTypeListProps + extends ListProps, + ListActions, + SortPage { + pageTypes: PageTypeList_pageTypes_edges_node[]; +} + +const numberOfColumns = 2; + +const PageTypeList: React.FC = props => { + const { + disabled, + pageTypes, + pageInfo, + onNextPage, + onPreviousPage, + onRowClick, + onSort, + isChecked, + selected, + sort, + toggle, + toggleAll, + toolbar + } = props; + const classes = useStyles(props); + + return ( + + + onSort(PageTypeListUrlSortField.name)} + className={classes.colName} + > + + + + + + + + + + {renderCollection( + pageTypes, + pageType => { + const isSelected = pageType ? isChecked(pageType.id) : false; + return ( + + + toggle(pageType.id)} + /> + + + {pageType ? ( + {pageType.name} + ) : ( + + )} + + + ); + }, + () => ( + + + + + + ) + )} + + + ); +}; +PageTypeList.displayName = "PageTypeList"; +export default PageTypeList; diff --git a/src/pageTypes/components/PageTypeList/index.ts b/src/pageTypes/components/PageTypeList/index.ts new file mode 100644 index 000000000..5b03e9fb6 --- /dev/null +++ b/src/pageTypes/components/PageTypeList/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeList"; +export * from "./PageTypeList"; diff --git a/src/pageTypes/components/PageTypeListPage/PageTypeListPage.stories.tsx b/src/pageTypes/components/PageTypeListPage/PageTypeListPage.stories.tsx new file mode 100644 index 000000000..d1175627b --- /dev/null +++ b/src/pageTypes/components/PageTypeListPage/PageTypeListPage.stories.tsx @@ -0,0 +1,36 @@ +import { PageTypeListUrlSortField } from "@saleor/pageTypes/urls"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { + listActionsProps, + pageListProps, + searchPageProps, + sortPageProps, + tabPageProps +} from "../../../fixtures"; +import Decorator from "../../../storybook/Decorator"; +import { pageTypes } from "../../fixtures"; +import PageTypeListPage, { PageTypeListPageProps } from "."; + +const props: PageTypeListPageProps = { + ...listActionsProps, + ...pageListProps.default, + ...searchPageProps, + ...sortPageProps, + sort: { + ...sortPageProps.sort, + sort: PageTypeListUrlSortField.name + }, + ...tabPageProps, + onBack: () => undefined, + pageTypes +}; + +storiesOf("Views / Page types / Page types list", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ( + + )) + .add("no data", () => ); diff --git a/src/pageTypes/components/PageTypeListPage/PageTypeListPage.tsx b/src/pageTypes/components/PageTypeListPage/PageTypeListPage.tsx new file mode 100644 index 000000000..9d7260f40 --- /dev/null +++ b/src/pageTypes/components/PageTypeListPage/PageTypeListPage.tsx @@ -0,0 +1,83 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import AppHeader from "@saleor/components/AppHeader"; +import Container from "@saleor/components/Container"; +import PageHeader from "@saleor/components/PageHeader"; +import SearchBar from "@saleor/components/SearchBar"; +import { sectionNames } from "@saleor/intl"; +import { PageTypeList_pageTypes_edges_node } from "@saleor/pageTypes/types/PageTypeList"; +import { PageTypeListUrlSortField } from "@saleor/pageTypes/urls"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { + ListActions, + PageListProps, + SearchPageProps, + SortPage, + TabPageProps +} from "../../../types"; +import PageTypeList from "../PageTypeList"; + +export interface PageTypeListPageProps + extends PageListProps, + ListActions, + SearchPageProps, + SortPage, + TabPageProps { + pageTypes: PageTypeList_pageTypes_edges_node[]; + onBack: () => void; +} + +const PageTypeListPage: React.FC = ({ + currentTab, + initialSearch, + onAdd, + onAll, + onBack, + onSearchChange, + onTabChange, + onTabDelete, + onTabSave, + tabs, + ...listProps +}) => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(sectionNames.configuration)} + + + + + + + + + + ); +}; +PageTypeListPage.displayName = "PageTypeListPage"; +export default PageTypeListPage; diff --git a/src/pageTypes/components/PageTypeListPage/index.ts b/src/pageTypes/components/PageTypeListPage/index.ts new file mode 100644 index 000000000..d6d83d611 --- /dev/null +++ b/src/pageTypes/components/PageTypeListPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeListPage"; +export * from "./PageTypeListPage"; diff --git a/src/pageTypes/fixtures.ts b/src/pageTypes/fixtures.ts new file mode 100644 index 000000000..d6ea96ef7 --- /dev/null +++ b/src/pageTypes/fixtures.ts @@ -0,0 +1,73 @@ +/* eslint-disable sort-keys */ +import { AttributeTypeEnum } from "@saleor/types/globalTypes"; + +import { PageTypeDetails_pageType } from "./types/PageTypeDetails"; +import { PageTypeList_pageTypes_edges_node } from "./types/PageTypeList"; + +export const pageTypes: PageTypeList_pageTypes_edges_node[] = [ + { + id: "UGFnZVR5cGU6MQ==", + name: "Blog", + hasPages: true, + __typename: "PageType" + }, + { + id: "UGFnZVR5cGU6Mw==", + name: "Landing Page", + hasPages: true, + __typename: "PageType" + }, + { + id: "UGFnZVR5cGU6Mg==", + name: "Marketing Page", + hasPages: false, + __typename: "PageType" + } +]; + +export const pageType: PageTypeDetails_pageType = { + id: "UGFnZVR5cGU6MQ==", + __typename: "PageType", + metadata: [ + { + __typename: "MetadataItem", + key: "integration.id", + value: "100023123" + } + ], + name: "Blog", + hasPages: true, + attributes: [ + { + __typename: "Attribute" as "Attribute", + id: "UHJvZHVjdEF0dHJpYnV0ZTo5", + name: "Author", + slug: "author", + visibleInStorefront: true, + filterableInDashboard: true, + filterableInStorefront: true, + type: AttributeTypeEnum.PAGE_TYPE + }, + { + __typename: "Attribute" as "Attribute", + id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==", + name: "Language", + slug: "language", + visibleInStorefront: true, + filterableInDashboard: true, + filterableInStorefront: true, + type: AttributeTypeEnum.PAGE_TYPE + }, + { + __typename: "Attribute" as "Attribute", + id: "UHJvZHVjdEF0dHJpYnV0ZTo5", + name: "Author", + slug: "author", + visibleInStorefront: true, + filterableInDashboard: true, + filterableInStorefront: true, + type: AttributeTypeEnum.PAGE_TYPE + } + ], + privateMetadata: [] +}; diff --git a/src/pageTypes/hooks/useAvailablePageAttributeSearch/index.tsx b/src/pageTypes/hooks/useAvailablePageAttributeSearch/index.tsx new file mode 100644 index 000000000..1a9f99c2e --- /dev/null +++ b/src/pageTypes/hooks/useAvailablePageAttributeSearch/index.tsx @@ -0,0 +1,72 @@ +import { availableAttributeFragment } from "@saleor/fragments/attributes"; +import { pageInfoFragment } from "@saleor/fragments/pageInfo"; +import makeSearch from "@saleor/hooks/makeSearch"; +import gql from "graphql-tag"; + +import { + SearchAvailablePageAttributes, + SearchAvailablePageAttributesVariables +} from "./types/SearchAvailablePageAttributes"; + +export const searchPageAttributes = gql` + ${pageInfoFragment} + ${availableAttributeFragment} + query SearchAvailablePageAttributes( + $id: ID! + $after: String + $first: Int! + $query: String! + ) { + pageType(id: $id) { + id + availableAttributes( + after: $after + first: $first + filter: { search: $query } + ) { + edges { + node { + ...AvailableAttributeFragment + } + } + pageInfo { + ...PageInfoFragment + } + } + } + } +`; + +export default makeSearch< + SearchAvailablePageAttributes, + SearchAvailablePageAttributesVariables +>(searchPageAttributes, result => + result.loadMore( + (prev, next) => { + if ( + prev.pageType.availableAttributes.pageInfo.endCursor === + next.pageType.availableAttributes.pageInfo.endCursor + ) { + return prev; + } + + return { + ...prev, + pageType: { + ...prev.pageType, + availableAttributes: { + ...prev.pageType.availableAttributes, + edges: [ + ...prev.pageType.availableAttributes.edges, + ...next.pageType.availableAttributes.edges + ], + pageInfo: next.pageType.availableAttributes.pageInfo + } + } + }; + }, + { + after: result.data.pageType.availableAttributes.pageInfo.endCursor + } + ) +); diff --git a/src/pageTypes/hooks/useAvailablePageAttributeSearch/types/SearchAvailablePageAttributes.ts b/src/pageTypes/hooks/useAvailablePageAttributeSearch/types/SearchAvailablePageAttributes.ts new file mode 100644 index 000000000..7e2f7f9cc --- /dev/null +++ b/src/pageTypes/hooks/useAvailablePageAttributeSearch/types/SearchAvailablePageAttributes.ts @@ -0,0 +1,50 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: SearchAvailablePageAttributes +// ==================================================== + +export interface SearchAvailablePageAttributes_pageType_availableAttributes_edges_node { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; +} + +export interface SearchAvailablePageAttributes_pageType_availableAttributes_edges { + __typename: "AttributeCountableEdge"; + node: SearchAvailablePageAttributes_pageType_availableAttributes_edges_node; +} + +export interface SearchAvailablePageAttributes_pageType_availableAttributes_pageInfo { + __typename: "PageInfo"; + endCursor: string | null; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; +} + +export interface SearchAvailablePageAttributes_pageType_availableAttributes { + __typename: "AttributeCountableConnection"; + edges: SearchAvailablePageAttributes_pageType_availableAttributes_edges[]; + pageInfo: SearchAvailablePageAttributes_pageType_availableAttributes_pageInfo; +} + +export interface SearchAvailablePageAttributes_pageType { + __typename: "PageType"; + id: string; + availableAttributes: SearchAvailablePageAttributes_pageType_availableAttributes | null; +} + +export interface SearchAvailablePageAttributes { + pageType: SearchAvailablePageAttributes_pageType | null; +} + +export interface SearchAvailablePageAttributesVariables { + id: string; + after?: string | null; + first: number; + query: string; +} diff --git a/src/pageTypes/index.tsx b/src/pageTypes/index.tsx new file mode 100644 index 000000000..2d0d99a22 --- /dev/null +++ b/src/pageTypes/index.tsx @@ -0,0 +1,62 @@ +import { sectionNames } from "@saleor/intl"; +import { asSortParams } from "@saleor/utils/sort"; +import { parse as parseQs } from "qs"; +import React from "react"; +import { useIntl } from "react-intl"; +import { Route, RouteComponentProps, Switch } from "react-router-dom"; + +import { WindowTitle } from "../components/WindowTitle"; +import { + pageTypeAddPath, + pageTypeListPath, + PageTypeListUrlQueryParams, + PageTypeListUrlSortField, + pageTypePath, + PageTypeUrlQueryParams +} from "./urls"; +import PageTypeCreate from "./views/PageTypeCreate"; +import PageTypeDetailsComponent from "./views/PageTypeDetails"; +import PageTypeListComponent from "./views/PageTypeList"; + +const PageTypeList: React.FC> = ({ location }) => { + const qs = parseQs(location.search.substr(1)); + const params: PageTypeListUrlQueryParams = asSortParams( + qs, + PageTypeListUrlSortField + ); + return ; +}; + +interface PageTypeDetailsRouteParams { + id: string; +} +const PageTypeDetails: React.FC> = ({ match }) => { + const qs = parseQs(location.search.substr(1)); + const params: PageTypeUrlQueryParams = qs; + + return ( + + ); +}; + +export const PageTypeRouter: React.FC = () => { + const intl = useIntl(); + + return ( + <> + + + + + + + + ); +}; +PageTypeRouter.displayName = "PageTypeRouter"; +export default PageTypeRouter; diff --git a/src/pageTypes/mutations.ts b/src/pageTypes/mutations.ts new file mode 100644 index 000000000..ee8f130b3 --- /dev/null +++ b/src/pageTypes/mutations.ts @@ -0,0 +1,161 @@ +import { pageErrorFragment } from "@saleor/fragments/errors"; +import { pageTypeDetailsFragment } from "@saleor/fragments/pageTypes"; +import makeMutation from "@saleor/hooks/makeMutation"; +import gql from "graphql-tag"; + +import { + AssignPageAttribute, + AssignPageAttributeVariables +} from "./types/AssignPageAttribute"; +import { + PageTypeAttributeReorder, + PageTypeAttributeReorderVariables +} from "./types/PageTypeAttributeReorder"; +import { + PageTypeBulkDelete, + PageTypeBulkDeleteVariables +} from "./types/PageTypeBulkDelete"; +import { + PageTypeCreate, + PageTypeCreateVariables +} from "./types/PageTypeCreate"; +import { + PageTypeDelete, + PageTypeDeleteVariables +} from "./types/PageTypeDelete"; +import { + PageTypeUpdate, + PageTypeUpdateVariables +} from "./types/PageTypeUpdate"; +import { + UnassignPageAttribute, + UnassignPageAttributeVariables +} from "./types/UnassignPageAttribute"; + +export const pageTypeUpdateMutation = gql` + ${pageTypeDetailsFragment} + ${pageErrorFragment} + mutation PageTypeUpdate($id: ID!, $input: PageTypeUpdateInput!) { + pageTypeUpdate(id: $id, input: $input) { + errors: pageErrors { + ...PageErrorFragment + } + pageType { + ...PageTypeDetailsFragment + } + } + } +`; +export const usePageTypeUpdateMutation = makeMutation< + PageTypeUpdate, + PageTypeUpdateVariables +>(pageTypeUpdateMutation); + +export const pageTypeCreateMutation = gql` + ${pageTypeDetailsFragment} + ${pageErrorFragment} + mutation PageTypeCreate($input: PageTypeCreateInput!) { + pageTypeCreate(input: $input) { + errors: pageErrors { + ...PageErrorFragment + } + pageType { + ...PageTypeDetailsFragment + } + } + } +`; +export const usePageTypeCreateMutation = makeMutation< + PageTypeCreate, + PageTypeCreateVariables +>(pageTypeCreateMutation); + +export const assignPageAttributeMutation = gql` + ${pageTypeDetailsFragment} + ${pageErrorFragment} + mutation AssignPageAttribute($id: ID!, $ids: [ID!]!) { + pageAttributeAssign(pageTypeId: $id, attributeIds: $ids) { + errors: pageErrors { + ...PageErrorFragment + } + pageType { + ...PageTypeDetailsFragment + } + } + } +`; +export const useAssignPageAttributeMutation = makeMutation< + AssignPageAttribute, + AssignPageAttributeVariables +>(assignPageAttributeMutation); + +export const unassignPageAttributeMutation = gql` + ${pageTypeDetailsFragment} + ${pageErrorFragment} + mutation UnassignPageAttribute($id: ID!, $ids: [ID!]!) { + pageAttributeUnassign(pageTypeId: $id, attributeIds: $ids) { + errors: pageErrors { + ...PageErrorFragment + } + pageType { + ...PageTypeDetailsFragment + } + } + } +`; +export const useUnassignPageAttributeMutation = makeMutation< + UnassignPageAttribute, + UnassignPageAttributeVariables +>(unassignPageAttributeMutation); + +export const pageTypeDeleteMutation = gql` + mutation PageTypeDelete($id: ID!) { + pageTypeDelete(id: $id) { + errors: pageErrors { + field + message + } + pageType { + id + } + } + } +`; +export const usePageTypeDeleteMutation = makeMutation< + PageTypeDelete, + PageTypeDeleteVariables +>(pageTypeDeleteMutation); + +export const pageTypeBulkDeleteMutation = gql` + mutation PageTypeBulkDelete($ids: [ID!]!) { + pageTypeBulkDelete(ids: $ids) { + errors: pageErrors { + field + message + } + } + } +`; +export const usePageTypeBulkDeleteMutation = makeMutation< + PageTypeBulkDelete, + PageTypeBulkDeleteVariables +>(pageTypeBulkDeleteMutation); + +export const pageTypeAttributeReorder = gql` + ${pageTypeDetailsFragment} + ${pageErrorFragment} + mutation PageTypeAttributeReorder($move: ReorderInput!, $pageTypeId: ID!) { + pageTypeReorderAttributes(moves: [$move], pageTypeId: $pageTypeId) { + errors: pageErrors { + ...PageErrorFragment + } + pageType { + ...PageTypeDetailsFragment + } + } + } +`; +export const usePageTypeAttributeReorderMutation = makeMutation< + PageTypeAttributeReorder, + PageTypeAttributeReorderVariables +>(pageTypeAttributeReorder); diff --git a/src/pageTypes/queries.ts b/src/pageTypes/queries.ts new file mode 100644 index 000000000..7e2e2d584 --- /dev/null +++ b/src/pageTypes/queries.ts @@ -0,0 +1,63 @@ +import { pageInfoFragment } from "@saleor/fragments/pageInfo"; +import { + pageTypeDetailsFragment, + pageTypeFragment +} from "@saleor/fragments/pageTypes"; +import makeQuery from "@saleor/hooks/makeQuery"; +import gql from "graphql-tag"; + +import { + PageTypeDetails, + PageTypeDetailsVariables +} from "./types/PageTypeDetails"; +import { PageTypeList, PageTypeListVariables } from "./types/PageTypeList"; + +export const pageTypeListQuery = gql` + ${pageInfoFragment} + ${pageTypeFragment} + query PageTypeList( + $after: String + $before: String + $first: Int + $last: Int + $filter: PageTypeFilterInput + $sort: PageTypeSortingInput + ) { + pageTypes( + after: $after + before: $before + first: $first + last: $last + filter: $filter + sortBy: $sort + ) { + edges { + node { + ...PageTypeFragment + hasPages + } + } + pageInfo { + ...PageInfoFragment + } + } + } +`; +export const usePageTypeListQuery = makeQuery< + PageTypeList, + PageTypeListVariables +>(pageTypeListQuery); + +export const pageTypeDetailsQuery = gql` + ${pageTypeDetailsFragment} + query PageTypeDetails($id: ID!) { + pageType(id: $id) { + ...PageTypeDetailsFragment + hasPages + } + } +`; +export const usePageTypeDetailsQuery = makeQuery< + PageTypeDetails, + PageTypeDetailsVariables +>(pageTypeDetailsQuery); diff --git a/src/pageTypes/types/AssignPageAttribute.ts b/src/pageTypes/types/AssignPageAttribute.ts new file mode 100644 index 000000000..167fb00bf --- /dev/null +++ b/src/pageTypes/types/AssignPageAttribute.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageErrorCode, AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: AssignPageAttribute +// ==================================================== + +export interface AssignPageAttribute_pageAttributeAssign_errors { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; +} + +export interface AssignPageAttribute_pageAttributeAssign_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AssignPageAttribute_pageAttributeAssign_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface AssignPageAttribute_pageAttributeAssign_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface AssignPageAttribute_pageAttributeAssign_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (AssignPageAttribute_pageAttributeAssign_pageType_metadata | null)[]; + privateMetadata: (AssignPageAttribute_pageAttributeAssign_pageType_privateMetadata | null)[]; + attributes: (AssignPageAttribute_pageAttributeAssign_pageType_attributes | null)[] | null; +} + +export interface AssignPageAttribute_pageAttributeAssign { + __typename: "PageAttributeAssign"; + errors: AssignPageAttribute_pageAttributeAssign_errors[]; + pageType: AssignPageAttribute_pageAttributeAssign_pageType | null; +} + +export interface AssignPageAttribute { + pageAttributeAssign: AssignPageAttribute_pageAttributeAssign | null; +} + +export interface AssignPageAttributeVariables { + id: string; + ids: string[]; +} diff --git a/src/pageTypes/types/PageTypeAttributeReorder.ts b/src/pageTypes/types/PageTypeAttributeReorder.ts new file mode 100644 index 000000000..101ee25a8 --- /dev/null +++ b/src/pageTypes/types/PageTypeAttributeReorder.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { ReorderInput, PageErrorCode, AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: PageTypeAttributeReorder +// ==================================================== + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes_errors { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; +} + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_metadata | null)[]; + privateMetadata: (PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_privateMetadata | null)[]; + attributes: (PageTypeAttributeReorder_pageTypeReorderAttributes_pageType_attributes | null)[] | null; +} + +export interface PageTypeAttributeReorder_pageTypeReorderAttributes { + __typename: "PageTypeReorderAttributes"; + errors: PageTypeAttributeReorder_pageTypeReorderAttributes_errors[]; + pageType: PageTypeAttributeReorder_pageTypeReorderAttributes_pageType | null; +} + +export interface PageTypeAttributeReorder { + pageTypeReorderAttributes: PageTypeAttributeReorder_pageTypeReorderAttributes | null; +} + +export interface PageTypeAttributeReorderVariables { + move: ReorderInput; + pageTypeId: string; +} diff --git a/src/pageTypes/types/PageTypeBulkDelete.ts b/src/pageTypes/types/PageTypeBulkDelete.ts new file mode 100644 index 000000000..12e544e87 --- /dev/null +++ b/src/pageTypes/types/PageTypeBulkDelete.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: PageTypeBulkDelete +// ==================================================== + +export interface PageTypeBulkDelete_pageTypeBulkDelete_errors { + __typename: "PageError"; + field: string | null; + message: string | null; +} + +export interface PageTypeBulkDelete_pageTypeBulkDelete { + __typename: "PageTypeBulkDelete"; + errors: PageTypeBulkDelete_pageTypeBulkDelete_errors[]; +} + +export interface PageTypeBulkDelete { + pageTypeBulkDelete: PageTypeBulkDelete_pageTypeBulkDelete | null; +} + +export interface PageTypeBulkDeleteVariables { + ids: string[]; +} diff --git a/src/pageTypes/types/PageTypeCreate.ts b/src/pageTypes/types/PageTypeCreate.ts new file mode 100644 index 000000000..5f4fa9264 --- /dev/null +++ b/src/pageTypes/types/PageTypeCreate.ts @@ -0,0 +1,61 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageTypeCreateInput, PageErrorCode, AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: PageTypeCreate +// ==================================================== + +export interface PageTypeCreate_pageTypeCreate_errors { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; +} + +export interface PageTypeCreate_pageTypeCreate_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeCreate_pageTypeCreate_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeCreate_pageTypeCreate_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface PageTypeCreate_pageTypeCreate_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (PageTypeCreate_pageTypeCreate_pageType_metadata | null)[]; + privateMetadata: (PageTypeCreate_pageTypeCreate_pageType_privateMetadata | null)[]; + attributes: (PageTypeCreate_pageTypeCreate_pageType_attributes | null)[] | null; +} + +export interface PageTypeCreate_pageTypeCreate { + __typename: "PageTypeCreate"; + errors: PageTypeCreate_pageTypeCreate_errors[]; + pageType: PageTypeCreate_pageTypeCreate_pageType | null; +} + +export interface PageTypeCreate { + pageTypeCreate: PageTypeCreate_pageTypeCreate | null; +} + +export interface PageTypeCreateVariables { + input: PageTypeCreateInput; +} diff --git a/src/pageTypes/types/PageTypeDelete.ts b/src/pageTypes/types/PageTypeDelete.ts new file mode 100644 index 000000000..a4e2b41b0 --- /dev/null +++ b/src/pageTypes/types/PageTypeDelete.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: PageTypeDelete +// ==================================================== + +export interface PageTypeDelete_pageTypeDelete_errors { + __typename: "PageError"; + field: string | null; + message: string | null; +} + +export interface PageTypeDelete_pageTypeDelete_pageType { + __typename: "PageType"; + id: string; +} + +export interface PageTypeDelete_pageTypeDelete { + __typename: "PageTypeDelete"; + errors: PageTypeDelete_pageTypeDelete_errors[]; + pageType: PageTypeDelete_pageTypeDelete_pageType | null; +} + +export interface PageTypeDelete { + pageTypeDelete: PageTypeDelete_pageTypeDelete | null; +} + +export interface PageTypeDeleteVariables { + id: string; +} diff --git a/src/pageTypes/types/PageTypeDetails.ts b/src/pageTypes/types/PageTypeDetails.ts new file mode 100644 index 000000000..aa6ea4238 --- /dev/null +++ b/src/pageTypes/types/PageTypeDetails.ts @@ -0,0 +1,50 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: PageTypeDetails +// ==================================================== + +export interface PageTypeDetails_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeDetails_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeDetails_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface PageTypeDetails_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (PageTypeDetails_pageType_metadata | null)[]; + privateMetadata: (PageTypeDetails_pageType_privateMetadata | null)[]; + attributes: (PageTypeDetails_pageType_attributes | null)[] | null; + hasPages: boolean | null; +} + +export interface PageTypeDetails { + pageType: PageTypeDetails_pageType | null; +} + +export interface PageTypeDetailsVariables { + id: string; +} diff --git a/src/pageTypes/types/PageTypeList.ts b/src/pageTypes/types/PageTypeList.ts new file mode 100644 index 000000000..827527d55 --- /dev/null +++ b/src/pageTypes/types/PageTypeList.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageTypeFilterInput, PageTypeSortingInput } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: PageTypeList +// ==================================================== + +export interface PageTypeList_pageTypes_edges_node { + __typename: "PageType"; + id: string; + name: string; + hasPages: boolean | null; +} + +export interface PageTypeList_pageTypes_edges { + __typename: "PageTypeCountableEdge"; + node: PageTypeList_pageTypes_edges_node; +} + +export interface PageTypeList_pageTypes_pageInfo { + __typename: "PageInfo"; + endCursor: string | null; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; +} + +export interface PageTypeList_pageTypes { + __typename: "PageTypeCountableConnection"; + edges: PageTypeList_pageTypes_edges[]; + pageInfo: PageTypeList_pageTypes_pageInfo; +} + +export interface PageTypeList { + pageTypes: PageTypeList_pageTypes | null; +} + +export interface PageTypeListVariables { + after?: string | null; + before?: string | null; + first?: number | null; + last?: number | null; + filter?: PageTypeFilterInput | null; + sort?: PageTypeSortingInput | null; +} diff --git a/src/pageTypes/types/PageTypeUpdate.ts b/src/pageTypes/types/PageTypeUpdate.ts new file mode 100644 index 000000000..5aa97089a --- /dev/null +++ b/src/pageTypes/types/PageTypeUpdate.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageTypeUpdateInput, PageErrorCode, AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: PageTypeUpdate +// ==================================================== + +export interface PageTypeUpdate_pageTypeUpdate_errors { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; +} + +export interface PageTypeUpdate_pageTypeUpdate_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeUpdate_pageTypeUpdate_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface PageTypeUpdate_pageTypeUpdate_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface PageTypeUpdate_pageTypeUpdate_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (PageTypeUpdate_pageTypeUpdate_pageType_metadata | null)[]; + privateMetadata: (PageTypeUpdate_pageTypeUpdate_pageType_privateMetadata | null)[]; + attributes: (PageTypeUpdate_pageTypeUpdate_pageType_attributes | null)[] | null; +} + +export interface PageTypeUpdate_pageTypeUpdate { + __typename: "PageTypeUpdate"; + errors: PageTypeUpdate_pageTypeUpdate_errors[]; + pageType: PageTypeUpdate_pageTypeUpdate_pageType | null; +} + +export interface PageTypeUpdate { + pageTypeUpdate: PageTypeUpdate_pageTypeUpdate | null; +} + +export interface PageTypeUpdateVariables { + id: string; + input: PageTypeUpdateInput; +} diff --git a/src/pageTypes/types/UnassignPageAttribute.ts b/src/pageTypes/types/UnassignPageAttribute.ts new file mode 100644 index 000000000..131583882 --- /dev/null +++ b/src/pageTypes/types/UnassignPageAttribute.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PageErrorCode, AttributeTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: UnassignPageAttribute +// ==================================================== + +export interface UnassignPageAttribute_pageAttributeUnassign_errors { + __typename: "PageError"; + code: PageErrorCode; + field: string | null; +} + +export interface UnassignPageAttribute_pageAttributeUnassign_pageType_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface UnassignPageAttribute_pageAttributeUnassign_pageType_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface UnassignPageAttribute_pageAttributeUnassign_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + type: AttributeTypeEnum | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; +} + +export interface UnassignPageAttribute_pageAttributeUnassign_pageType { + __typename: "PageType"; + id: string; + name: string; + metadata: (UnassignPageAttribute_pageAttributeUnassign_pageType_metadata | null)[]; + privateMetadata: (UnassignPageAttribute_pageAttributeUnassign_pageType_privateMetadata | null)[]; + attributes: (UnassignPageAttribute_pageAttributeUnassign_pageType_attributes | null)[] | null; +} + +export interface UnassignPageAttribute_pageAttributeUnassign { + __typename: "PageAttributeUnassign"; + errors: UnassignPageAttribute_pageAttributeUnassign_errors[]; + pageType: UnassignPageAttribute_pageAttributeUnassign_pageType | null; +} + +export interface UnassignPageAttribute { + pageAttributeUnassign: UnassignPageAttribute_pageAttributeUnassign | null; +} + +export interface UnassignPageAttributeVariables { + id: string; + ids: string[]; +} diff --git a/src/pageTypes/urls.ts b/src/pageTypes/urls.ts new file mode 100644 index 000000000..46860e4a4 --- /dev/null +++ b/src/pageTypes/urls.ts @@ -0,0 +1,52 @@ +import { stringify as stringifyQs } from "qs"; +import urlJoin from "url-join"; + +import { + ActiveTab, + BulkAction, + Dialog, + Filters, + Pagination, + SingleAction, + Sort, + TabActionDialog +} from "../types"; + +const pageTypeSection = "/page-types/"; + +export const pageTypeListPath = pageTypeSection; +export enum PageTypeListUrlFiltersEnum { + type = "type", + query = "query" +} +export type PageTypeListUrlFilters = Filters; +export type PageTypeListUrlDialog = "remove" | TabActionDialog; +export enum PageTypeListUrlSortField { + name = "name" +} +export type PageTypeListUrlSort = Sort; +export type PageTypeListUrlQueryParams = ActiveTab & + BulkAction & + Dialog & + Pagination & + PageTypeListUrlFilters & + PageTypeListUrlSort; +export const pageTypeListUrl = (params?: PageTypeListUrlQueryParams) => + pageTypeListPath + "?" + stringifyQs(params); + +export const pageTypeAddPath = urlJoin(pageTypeSection, "add"); +export const pageTypeAddUrl = pageTypeAddPath; + +export const pageTypePath = (id: string) => urlJoin(pageTypeSection, id); +export type PageTypeUrlDialog = + | "assign-attribute" + | "unassign-attribute" + | "unassign-attributes" + | "remove"; +export type PageTypeUrlQueryParams = BulkAction & + Dialog & + SingleAction & { + type?: string; + }; +export const pageTypeUrl = (id: string, params?: PageTypeUrlQueryParams) => + pageTypePath(encodeURIComponent(id)) + "?" + stringifyQs(params); diff --git a/src/pageTypes/views/PageTypeCreate.tsx b/src/pageTypes/views/PageTypeCreate.tsx new file mode 100644 index 000000000..f1a056ac9 --- /dev/null +++ b/src/pageTypes/views/PageTypeCreate.tsx @@ -0,0 +1,76 @@ +import { WindowTitle } from "@saleor/components/WindowTitle"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; +import { + useMetadataUpdate, + usePrivateMetadataUpdate +} from "@saleor/utils/metadata/updateMetadata"; +import React from "react"; +import { useIntl } from "react-intl"; + +import PageTypeCreatePage, { + PageTypeForm +} from "../components/PageTypeCreatePage"; +import { usePageTypeCreateMutation } from "../mutations"; +import { PageTypeCreate as PageTypeCreateMutation } from "../types/PageTypeCreate"; +import { pageTypeListUrl, pageTypeUrl } from "../urls"; + +export const PageTypeCreate: React.FC = () => { + const navigate = useNavigator(); + const notify = useNotifier(); + const intl = useIntl(); + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); + + const [createPageType, createPageTypeOpts] = usePageTypeCreateMutation({ + onCompleted: (updateData: PageTypeCreateMutation) => { + if (updateData.pageTypeCreate.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Successfully created page type" + }) + }); + navigate(pageTypeUrl(updateData.pageTypeCreate.pageType.id)); + } + } + }); + + const handleCreate = async (formData: PageTypeForm) => { + const result = await createPageType({ + variables: { + input: { + name: formData.name + } + } + }); + + return result.data?.pageTypeCreate.pageType?.id || null; + }; + const handleSubmit = createMetadataCreateHandler( + handleCreate, + updateMetadata, + updatePrivateMetadata + ); + + return ( + <> + + navigate(pageTypeListUrl())} + onSubmit={handleSubmit} + /> + + ); +}; +export default PageTypeCreate; diff --git a/src/pageTypes/views/PageTypeDetails.tsx b/src/pageTypes/views/PageTypeDetails.tsx new file mode 100644 index 000000000..7d2c0c32d --- /dev/null +++ b/src/pageTypes/views/PageTypeDetails.tsx @@ -0,0 +1,324 @@ +import Button from "@material-ui/core/Button"; +import { attributeUrl } from "@saleor/attributes/urls"; +import AssignAttributeDialog from "@saleor/components/AssignAttributeDialog"; +import AttributeUnassignDialog from "@saleor/components/AttributeUnassignDialog"; +import BulkAttributeUnassignDialog from "@saleor/components/BulkAttributeUnassignDialog"; +import NotFoundPage from "@saleor/components/NotFoundPage"; +import { WindowTitle } from "@saleor/components/WindowTitle"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; +import useBulkActions from "@saleor/hooks/useBulkActions"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import { commonMessages } from "@saleor/intl"; +import { getStringOrPlaceholder } from "@saleor/misc"; +import PageTypeDeleteDialog from "@saleor/pageTypes/components/PageTypeDeleteDialog"; +import { + useAssignPageAttributeMutation, + usePageTypeAttributeReorderMutation, + usePageTypeDeleteMutation, + usePageTypeUpdateMutation, + useUnassignPageAttributeMutation +} from "@saleor/pageTypes/mutations"; +import { ReorderEvent } from "@saleor/types"; +import getPageErrorMessage from "@saleor/utils/errors/page"; +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"; + +import PageTypeDetailsPage, { + PageTypeForm +} from "../components/PageTypeDetailsPage"; +import useAvailablePageAttributeSearch from "../hooks/useAvailablePageAttributeSearch"; +import { usePageTypeDetailsQuery } from "../queries"; +import { pageTypeListUrl, pageTypeUrl, PageTypeUrlQueryParams } from "../urls"; + +interface PageTypeDetailsProps { + id: string; + params: PageTypeUrlQueryParams; +} + +export const PageTypeDetails: React.FC = ({ + id, + params +}) => { + const navigate = useNavigator(); + const notify = useNotifier(); + const attributeListActions = useBulkActions(); + const intl = useIntl(); + + const [updatePageType, updatePageTypeOpts] = usePageTypeUpdateMutation({ + onCompleted: updateData => { + if ( + !updateData.pageTypeUpdate.errors || + updateData.pageTypeUpdate.errors.length === 0 + ) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + } + } + }); + const [deletePageType, deletePageTypeOpts] = usePageTypeDeleteMutation({ + onCompleted: deleteData => { + if (deleteData.pageTypeDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Page type deleted" + }) + }); + navigate(pageTypeListUrl(), true); + } + } + }); + const [assignAttribute, assignAttributeOpts] = useAssignPageAttributeMutation( + { + onCompleted: data => { + if (data.pageAttributeAssign.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + } + } + } + ); + const [ + unassignAttribute, + unassignAttributeOpts + ] = useUnassignPageAttributeMutation({ + onCompleted: data => { + if (data.pageAttributeUnassign.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + attributeListActions.reset(); + } + } + }); + const [reorderAttribute] = usePageTypeAttributeReorderMutation({}); + + const [updateMetadata] = useMetadataUpdate({}); + const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); + + const handleBack = () => navigate(pageTypeListUrl()); + + const handlePageTypeUpdate = async (formData: PageTypeForm) => { + const result = await updatePageType({ + variables: { + id, + input: { + name: formData.name + } + } + }); + + return result.data.pageTypeUpdate.errors; + }; + const handlePageTypeDelete = () => deletePageType({ variables: { id } }); + const handleAssignAttribute = () => + assignAttribute({ + variables: { + id, + ids: params.ids + } + }); + const handleAttributeUnassign = () => + unassignAttribute({ + variables: { + id, + ids: [params.id] + } + }); + const handleBulkAttributeUnassign = () => + unassignAttribute({ + variables: { + id, + ids: params.ids + } + }); + const handleAttributeReorder = (event: ReorderEvent) => + reorderAttribute({ + variables: { + move: { + id: data.pageType.attributes[event.oldIndex].id, + sortOrder: event.newIndex - event.oldIndex + }, + pageTypeId: id + } + }); + + const { data, loading: dataLoading } = usePageTypeDetailsQuery({ + variables: { id } + }); + + const { loadMore, search, result } = useAvailablePageAttributeSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + id + } + }); + + const pageType = data?.pageType; + + if (pageType === null) { + return ; + } + + const closeModal = () => navigate(pageTypeUrl(id), true); + + const handleSubmit = createMetadataUpdateHandler( + data?.pageType, + handlePageTypeUpdate, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }) + ); + + const loading = updatePageTypeOpts.loading || dataLoading; + + return ( + <> + + + navigate( + pageTypeUrl(id, { + action: "assign-attribute", + type + }) + ) + } + onAttributeClick={attributeId => navigate(attributeUrl(attributeId))} + onAttributeReorder={handleAttributeReorder} + onAttributeUnassign={attributeId => + navigate( + pageTypeUrl(id, { + action: "unassign-attribute", + id: attributeId + }) + ) + } + onBack={handleBack} + onDelete={() => + navigate( + pageTypeUrl(id, { + action: "remove" + }) + ) + } + onSubmit={handleSubmit} + attributeList={{ + isChecked: attributeListActions.isSelected, + selected: attributeListActions.listElements.length, + toggle: attributeListActions.toggle, + toggleAll: attributeListActions.toggleAll, + toolbar: ( + + ) + }} + /> + navigate(pageTypeUrl(id))} + onConfirm={handlePageTypeDelete} + /> + {!dataLoading && ( + edge.node + )} + confirmButtonState={assignAttributeOpts.status} + errors={ + assignAttributeOpts.data?.pageAttributeAssign.errors + ? assignAttributeOpts.data.pageAttributeAssign.errors.map(err => + getPageErrorMessage(err, intl) + ) + : [] + } + loading={result.loading} + onClose={closeModal} + onSubmit={handleAssignAttribute} + onFetch={search} + onFetchMore={loadMore} + onOpen={result.refetch} + hasMore={ + !!result.data?.pageType.availableAttributes.pageInfo.hasNextPage + } + open={params.action === "assign-attribute"} + selected={params.ids || []} + onToggle={attributeId => { + const ids = params.ids || []; + navigate( + pageTypeUrl(id, { + ...params, + ids: ids.includes(attributeId) + ? params.ids.filter(selectedId => selectedId !== attributeId) + : [...ids, attributeId] + }) + ); + }} + /> + )} + + attribute.id === params.id + )?.name + )} + confirmButtonState={unassignAttributeOpts.status} + onClose={closeModal} + onConfirm={handleAttributeUnassign} + open={params.action === "unassign-attribute"} + itemTypeName={getStringOrPlaceholder(data?.pageType.name)} + /> + + ); +}; +export default PageTypeDetails; diff --git a/src/pageTypes/views/PageTypeList/PageTypeList.tsx b/src/pageTypes/views/PageTypeList/PageTypeList.tsx new file mode 100644 index 000000000..e4b56beb9 --- /dev/null +++ b/src/pageTypes/views/PageTypeList/PageTypeList.tsx @@ -0,0 +1,226 @@ +import IconButton from "@material-ui/core/IconButton"; +import DeleteIcon from "@material-ui/icons/Delete"; +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; +import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import usePaginator, { + createPaginationState +} from "@saleor/hooks/usePaginator"; +import { commonMessages } from "@saleor/intl"; +import { getStringOrPlaceholder } from "@saleor/misc"; +import PageTypeBulkDeleteDialog from "@saleor/pageTypes/components/PageTypeBulkDeleteDialog"; +import { usePageTypeBulkDeleteMutation } from "@saleor/pageTypes/mutations"; +import { ListViews } from "@saleor/types"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createSortHandler from "@saleor/utils/handlers/sortHandler"; +import { getSortParams } from "@saleor/utils/sort"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { configurationMenuUrl } from "../../../configuration"; +import PageTypeListPage from "../../components/PageTypeListPage"; +import { usePageTypeListQuery } from "../../queries"; +import { + pageTypeAddUrl, + pageTypeListUrl, + PageTypeListUrlDialog, + PageTypeListUrlFilters, + PageTypeListUrlQueryParams, + pageTypeUrl +} from "../../urls"; +import { + areFiltersApplied, + deleteFilterTab, + getActiveFilters, + getFilterTabs, + getFilterVariables, + saveFilterTab +} from "./filters"; +import { getSortQueryVariables } from "./sort"; + +interface PageTypeListProps { + params: PageTypeListUrlQueryParams; +} + +export const PageTypeList: React.FC = ({ params }) => { + const navigate = useNavigator(); + const paginate = usePaginator(); + const notify = useNotifier(); + const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( + params.ids + ); + const intl = useIntl(); + const { settings } = useListSettings(ListViews.PAGES_LIST); + + const paginationState = createPaginationState(settings.rowNumber, params); + const queryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: getFilterVariables(params), + sort: getSortQueryVariables(params) + }), + [params] + ); + const { data, loading, refetch } = usePageTypeListQuery({ + displayLoader: true, + variables: queryVariables + }); + + const tabs = getFilterTabs(); + + const currentTab = + params.activeTab === undefined + ? areFiltersApplied(params) + ? tabs.length + 1 + : 0 + : parseInt(params.activeTab, 0); + + const changeFilterField = (filter: PageTypeListUrlFilters) => { + reset(); + navigate( + pageTypeListUrl({ + ...getActiveFilters(params), + ...filter, + activeTab: undefined + }) + ); + }; + + const [openModal, closeModal] = createDialogActionHandlers< + PageTypeListUrlDialog, + PageTypeListUrlQueryParams + >(navigate, pageTypeListUrl, params); + + const handleTabChange = (tab: number) => { + reset(); + navigate( + pageTypeListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleTabDelete = () => { + deleteFilterTab(currentTab); + reset(); + navigate(pageTypeListUrl()); + }; + + const handleTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + data?.pageTypes?.pageInfo, + paginationState, + params + ); + + const handleSort = createSortHandler(navigate, pageTypeListUrl, params); + + const [ + pageTypeBulkDelete, + pageTypeBulkDeleteOpts + ] = usePageTypeBulkDeleteMutation({ + onCompleted: data => { + if (data.pageTypeBulkDelete.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + }); + reset(); + refetch(); + navigate( + pageTypeListUrl({ + ...params, + action: undefined, + ids: undefined + }) + ); + } + } + }); + + const hanldePageTypeBulkDelete = () => + pageTypeBulkDelete({ + variables: { + ids: params.ids + } + }); + + const selectedPageTypesHasPages = data?.pageTypes.edges.some( + pageType => + pageType.node.hasPages && params.ids?.some(id => id === pageType.node.id) + ); + + return ( + <> + changeFilterField({ query })} + onAll={() => navigate(pageTypeListUrl())} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} + disabled={loading} + pageTypes={data?.pageTypes?.edges?.map(edge => edge.node)} + pageInfo={pageInfo} + onAdd={() => navigate(pageTypeAddUrl)} + onBack={() => navigate(configurationMenuUrl)} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onRowClick={id => () => navigate(pageTypeUrl(id))} + onSort={handleSort} + isChecked={isSelected} + selected={listElements.length} + sort={getSortParams(params)} + toggle={toggle} + toggleAll={toggleAll} + toolbar={ + + openModal("remove", { + ids: listElements + }) + } + > + + + } + /> + + + + + ); +}; +PageTypeList.displayName = "PageTypeList"; +export default PageTypeList; diff --git a/src/pageTypes/views/PageTypeList/filters.ts b/src/pageTypes/views/PageTypeList/filters.ts new file mode 100644 index 000000000..c3b00d4e8 --- /dev/null +++ b/src/pageTypes/views/PageTypeList/filters.ts @@ -0,0 +1,32 @@ +import { PageTypeFilterInput } from "@saleor/types/globalTypes"; + +import { + createFilterTabUtils, + createFilterUtils +} from "../../../utils/filters"; +import { + PageTypeListUrlFilters, + PageTypeListUrlFiltersEnum, + PageTypeListUrlQueryParams +} from "../../urls"; + +export const PAGE_TYPE_FILTERS_KEY = "pageTypeFilters"; + +export function getFilterVariables( + params: PageTypeListUrlFilters +): PageTypeFilterInput { + return { + search: params.query + }; +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(PAGE_TYPE_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + PageTypeListUrlQueryParams, + PageTypeListUrlFilters +>(PageTypeListUrlFiltersEnum); diff --git a/src/pageTypes/views/PageTypeList/index.ts b/src/pageTypes/views/PageTypeList/index.ts new file mode 100644 index 000000000..5b03e9fb6 --- /dev/null +++ b/src/pageTypes/views/PageTypeList/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageTypeList"; +export * from "./PageTypeList"; diff --git a/src/pageTypes/views/PageTypeList/sort.ts b/src/pageTypes/views/PageTypeList/sort.ts new file mode 100644 index 000000000..e7599295a --- /dev/null +++ b/src/pageTypes/views/PageTypeList/sort.ts @@ -0,0 +1,18 @@ +import { PageTypeListUrlSortField } from "@saleor/pageTypes/urls"; +import { PageTypeSortField } from "@saleor/types/globalTypes"; +import { createGetSortQueryVariables } from "@saleor/utils/sort"; + +export function getSortQueryField( + sort: PageTypeListUrlSortField +): PageTypeSortField { + switch (sort) { + case PageTypeListUrlSortField.name: + return PageTypeSortField.NAME; + default: + return undefined; + } +} + +export const getSortQueryVariables = createGetSortQueryVariables( + getSortQueryField +); diff --git a/src/pageTypes/views/PageTypeUpdate/PageTypeUpdate.tsx b/src/pageTypes/views/PageTypeUpdate/PageTypeUpdate.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/components/PageAttributes/PageAttributes.tsx b/src/pages/components/PageAttributes/PageAttributes.tsx new file mode 100644 index 000000000..361ce3baa --- /dev/null +++ b/src/pages/components/PageAttributes/PageAttributes.tsx @@ -0,0 +1,247 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import IconButton from "@material-ui/core/IconButton"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Typography from "@material-ui/core/Typography"; +import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; +import CardTitle from "@saleor/components/CardTitle"; +import Grid from "@saleor/components/Grid"; +import Hr from "@saleor/components/Hr"; +import MultiAutocompleteSelectField, { + MultiAutocompleteChoiceType +} from "@saleor/components/MultiAutocompleteSelectField"; +import SingleAutocompleteSelectField, { + SingleAutocompleteChoiceType +} from "@saleor/components/SingleAutocompleteSelectField"; +import { PageDetailsFragment_pageType_attributes_values } from "@saleor/fragments/types/PageDetailsFragment"; +import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment"; +import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; +import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import getPageErrorMessage from "@saleor/utils/errors/page"; +import classNames from "classnames"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +export interface PageAttributeInputData { + inputType: AttributeInputTypeEnum; + isRequired: boolean; + values: PageDetailsFragment_pageType_attributes_values[]; +} +export type PageAttributeInput = FormsetAtomicData< + PageAttributeInputData, + string[] +>; +export interface PageAttributesProps { + attributes: PageAttributeInput[]; + disabled: boolean; + errors: PageErrorWithAttributesFragment[]; + onChange: FormsetChange; + onMultiChange: FormsetChange; +} + +const useStyles = makeStyles( + theme => ({ + attributeSection: { + "&:last-of-type": { + paddingBottom: 0 + }, + padding: theme.spacing(2, 0) + }, + attributeSectionLabel: { + alignItems: "center", + display: "flex" + }, + card: { + overflow: "visible" + }, + cardContent: { + "&:last-child": { + paddingBottom: theme.spacing(1) + }, + paddingTop: theme.spacing(1) + }, + expansionBar: { + display: "flex" + }, + expansionBarButton: { + marginBottom: theme.spacing(1) + }, + expansionBarButtonIcon: { + transition: theme.transitions.duration.short + "ms" + }, + expansionBarLabel: { + color: theme.palette.text.disabled, + fontSize: 14 + }, + expansionBarLabelContainer: { + alignItems: "center", + display: "flex", + flex: 1 + }, + rotate: { + transform: "rotate(180deg)" + } + }), + { name: "PageAttributes" } +); + +function getMultiChoices( + values: PageDetailsFragment_pageType_attributes_values[] +): MultiAutocompleteChoiceType[] { + return values.map(value => ({ + label: value.name, + value: value.slug + })); +} + +function getMultiDisplayValue( + attribute: PageAttributeInput +): MultiAutocompleteChoiceType[] { + return attribute.value.map(attributeValue => { + const definedAttributeValue = attribute.data.values.find( + definedValue => definedValue.slug === attributeValue + ); + if (!!definedAttributeValue) { + return { + label: definedAttributeValue.name, + value: definedAttributeValue.slug + }; + } + + return { + label: attributeValue, + value: attributeValue + }; + }); +} + +function getSingleChoices( + values: PageDetailsFragment_pageType_attributes_values[] +): SingleAutocompleteChoiceType[] { + return values.map(value => ({ + label: value.name, + value: value.slug + })); +} + +const PageAttributes: React.FC = ({ + attributes, + disabled, + errors, + onChange, + onMultiChange +}) => { + const intl = useIntl(); + const classes = useStyles({}); + const [expanded, setExpansionStatus] = React.useState(true); + const toggleExpansion = () => setExpansionStatus(!expanded); + + return ( + + + +
+
+ + + +
+ + + +
+ {expanded && attributes.length > 0 && ( + <> +
+ {attributes.map((attribute, attributeIndex) => { + const error = errors.find(err => + err.attributes?.includes(attribute.id) + ); + + return ( + + {attributeIndex > 0 &&
} + +
+ {attribute.label} +
+
+ {attribute.data.inputType === + AttributeInputTypeEnum.DROPDOWN ? ( + value.slug === attribute.value[0] + )?.name || + attribute.value[0] || + "" + } + emptyOption={!attribute.data.isRequired} + error={!!error} + helperText={getPageErrorMessage(error, intl)} + name={`attribute:${attribute.label}`} + label={intl.formatMessage({ + defaultMessage: "Value", + description: "attribute value" + })} + value={attribute.value[0]} + onChange={event => + onChange(attribute.id, event.target.value) + } + allowCustomValues={!attribute.data.isRequired} + /> + ) : ( + + onMultiChange(attribute.id, event.target.value) + } + allowCustomValues={!attribute.data.isRequired} + /> + )} +
+
+
+ ); + })} + + )} +
+
+ ); +}; +PageAttributes.displayName = "PageAttributes"; +export default PageAttributes; diff --git a/src/pages/components/PageAttributes/index.ts b/src/pages/components/PageAttributes/index.ts new file mode 100644 index 000000000..0b41faf1b --- /dev/null +++ b/src/pages/components/PageAttributes/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageAttributes"; +export * from "./PageAttributes"; diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 23e40fd48..93cac39a5 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -8,36 +8,46 @@ import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import VisibilityCard from "@saleor/components/VisibilityCard"; -import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; +import { SearchPageTypes_search_edges_node } from "@saleor/searches/types/SearchPageTypes"; +import { FetchMoreProps } from "@saleor/types"; import React from "react"; import { useIntl } from "react-intl"; import { PageDetails_page } from "../../types/PageDetails"; +import PageAttributes from "../PageAttributes"; import PageInfo from "../PageInfo"; +import PageOrganizeContent from "../PageOrganizeContent"; import PageForm, { PageData } from "./form"; export interface PageDetailsPageProps { disabled: boolean; - errors: PageErrorFragment[]; + errors: PageErrorWithAttributesFragment[]; page: PageDetails_page; + pageTypes?: SearchPageTypes_search_edges_node[]; allowEmptySlug?: boolean; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; onRemove: () => void; onSubmit: (data: PageData) => SubmitPromise; + fetchPageTypes?: (data: string) => void; + fetchMorePageTypes?: FetchMoreProps; } const PageDetailsPage: React.FC = ({ disabled, errors, page, + pageTypes, saveButtonBarState, onBack, onRemove, - onSubmit + onSubmit, + fetchPageTypes, + fetchMorePageTypes }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); @@ -45,8 +55,8 @@ const PageDetailsPage: React.FC = ({ const pageExists = page !== null; return ( - - {({ change, data, handlers, hasChanged, submit }) => ( + + {({ change, data, pageType, handlers, hasChanged, submit }) => ( {intl.formatMessage(sectionNames.pages)} @@ -88,6 +98,16 @@ const PageDetailsPage: React.FC = ({ })} /> + {data.attributes.length > 0 && ( + + )} +
@@ -117,6 +137,19 @@ const PageDetailsPage: React.FC = ({ }} onChange={change} /> + +
; + changeAttributeMulti: FormsetChange; } export interface UsePageUpdateFormResult { change: FormChange; data: PageData; + pageType: PageTypeFragment; handlers: PageUpdateHandlers; hasChanged: boolean; submit: () => void; @@ -38,21 +58,35 @@ export interface UsePageUpdateFormResult { export interface PageFormProps { children: (props: UsePageUpdateFormResult) => React.ReactNode; page: PageDetails_page; + pageTypes?: PageDetails_page_pageType[]; onSubmit: (data: PageData) => SubmitPromise; } function usePageForm( page: PageDetails_page, - onSubmit: (data: PageData) => SubmitPromise + onSubmit: (data: PageData) => SubmitPromise, + pageTypes?: PageDetails_page_pageType[] ): UsePageUpdateFormResult { const [changed, setChanged] = React.useState(false); const triggerChange = () => setChanged(true); const pageExists = page !== null; + const attributesFromPage = React.useMemo( + () => getAttributeInputFromPage(page), + [page] + ); + + const { + change: changeAttributeData, + data: attributes, + set: setAttributeData + } = useFormset(attributesFromPage || []); + const form = useForm({ isPublished: page?.isPublished, metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], + pageType: page?.pageType.id || "", privateMetadata: pageExists ? page?.privateMetadata?.map(mapMetadataItemToInput) : [], @@ -67,6 +101,10 @@ function usePageForm( triggerChange }); + const [pageType, setPageType] = useStateFromProps( + page?.pageType || null + ); + const { isMetadataModified, isPrivateMetadataModified, @@ -78,10 +116,26 @@ function usePageForm( triggerChange(); }; const changeMetadata = makeMetadataChangeHandler(handleChange); + const selectPageType = createPageTypeSelectHandler( + handleChange, + setAttributeData, + setPageType, + pageTypes + ); + const changeAttribute = createAttributeChangeHandler( + changeAttributeData, + triggerChange + ); + const changeAttributeMulti = createAttributeMultiChangeHandler( + changeAttributeData, + attributes, + triggerChange + ); // Need to make it function to always have content.current up to date const getData = (): PageData => ({ ...form.data, + attributes, content: content.current }); @@ -100,16 +154,25 @@ function usePageForm( change: handleChange, data: getData(), handlers: { + changeAttribute, + changeAttributeMulti, changeContent, - changeMetadata + changeMetadata, + selectPageType }, hasChanged: changed, + pageType, submit }; } -const PageForm: React.FC = ({ children, page, onSubmit }) => { - const props = usePageForm(page, onSubmit); +const PageForm: React.FC = ({ + children, + page, + pageTypes, + onSubmit +}) => { + const props = usePageForm(page, onSubmit, pageTypes); return
{children(props)}
; }; diff --git a/src/pages/components/PageOrganizeContent/PageOrganizeContent.tsx b/src/pages/components/PageOrganizeContent/PageOrganizeContent.tsx new file mode 100644 index 000000000..868ea4958 --- /dev/null +++ b/src/pages/components/PageOrganizeContent/PageOrganizeContent.tsx @@ -0,0 +1,102 @@ +import { makeStyles } from "@material-ui/core"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import CardTitle from "@saleor/components/CardTitle"; +import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; +import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { PageTypeFragment } from "@saleor/fragments/types/PageTypeFragment"; +import { FormChange } from "@saleor/hooks/useForm"; +import { FetchMoreProps } from "@saleor/types"; +import { getFormErrors } from "@saleor/utils/errors"; +import getPageErrorMessage from "@saleor/utils/errors/page"; +import { mapNodeToChoice } from "@saleor/utils/maps"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { PageFormData } from "../PageDetailsPage/form"; + +export interface PageOrganizeContentProps { + canChangeType: boolean; + data: PageFormData; + pageType?: PageTypeFragment; + pageTypeInputDisplayValue?: string; + errors: PageErrorFragment[]; + disabled: boolean; + pageTypes: PageTypeFragment[]; + onPageTypeChange?: FormChange; + fetchPageTypes?: (data: string) => void; + fetchMorePageTypes?: FetchMoreProps; +} + +const useStyles = makeStyles( + theme => ({ + label: { + marginBottom: theme.spacing(0.5) + } + }), + { name: "PageOrganizeContent" } +); + +const PageOrganizeContent: React.FC = props => { + const { + canChangeType, + data, + pageType, + pageTypeInputDisplayValue, + errors, + disabled, + pageTypes, + onPageTypeChange, + fetchPageTypes, + fetchMorePageTypes + } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + const formErrors = getFormErrors(["pageType"], errors); + + return ( + + + + {canChangeType ? ( + + ) : ( + <> + + + + {pageType?.name} + + )} + + + ); +}; +PageOrganizeContent.displayName = "PageOrganizeContent"; +export default PageOrganizeContent; diff --git a/src/pages/components/PageOrganizeContent/index.ts b/src/pages/components/PageOrganizeContent/index.ts new file mode 100644 index 000000000..36e2a67c5 --- /dev/null +++ b/src/pages/components/PageOrganizeContent/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PageOrganizeContent"; +export * from "./PageOrganizeContent"; diff --git a/src/pages/fixtures.ts b/src/pages/fixtures.ts index d912d5c09..bc010a95a 100644 --- a/src/pages/fixtures.ts +++ b/src/pages/fixtures.ts @@ -1,3 +1,6 @@ +/* eslint-disable sort-keys */ +import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; + import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { PageDetails_page } from "./types/PageDetails"; import { PageList_pages_edges_node } from "./types/PageList"; @@ -36,6 +39,92 @@ export const pageList: PageList_pages_edges_node[] = [ ]; export const page: PageDetails_page = { __typename: "Page", + attributes: [ + { + attribute: { + id: "QXR0cmlidXRlOjI3", + slug: "author", + name: "Author", + inputType: AttributeInputTypeEnum.DROPDOWN, + valueRequired: false, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6ODc=", + name: "Suzanne Ellison", + slug: "suzanne-ellison", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6ODg=", + name: "Dennis Perkins", + slug: "dennis-perkins", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6ODk=", + name: "Dylan Lamb", + slug: "dylan-lamb", + __typename: "AttributeValue" + } + ], + __typename: "Attribute" + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6ODk=", + name: "Dylan Lamb", + slug: "dylan-lamb", + __typename: "AttributeValue" + } + ], + __typename: "SelectedAttribute" + }, + { + attribute: { + id: "QXR0cmlidXRlOjI5", + slug: "tag", + name: "Tag", + inputType: AttributeInputTypeEnum.MULTISELECT, + valueRequired: false, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6OTA=", + name: "Security", + slug: "security", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTE=", + name: "Support", + slug: "support", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTI=", + name: "Medical", + slug: "medical", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTM=", + name: "General", + slug: "general", + __typename: "AttributeValue" + } + ], + __typename: "Attribute" + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6OTA=", + name: "Security", + slug: "security", + __typename: "AttributeValue" + } + ], + __typename: "SelectedAttribute" + } + ], contentJson: JSON.stringify(content), id: "Kzx152sEm==", isPublished: false, @@ -46,6 +135,73 @@ export const page: PageDetails_page = { value: "100023123" } ], + pageType: { + __typename: "PageType", + id: "UGFnZVR5cGU6MQ==", + name: "Blog", + attributes: [ + { + id: "QXR0cmlidXRlOjI3", + name: "Author", + inputType: AttributeInputTypeEnum.DROPDOWN, + valueRequired: false, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6ODc=", + name: "Suzanne Ellison", + slug: "suzanne-ellison", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6ODg=", + name: "Dennis Perkins", + slug: "dennis-perkins", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6ODk=", + name: "Dylan Lamb", + slug: "dylan-lamb", + __typename: "AttributeValue" + } + ], + __typename: "Attribute" + }, + { + id: "QXR0cmlidXRlOjI5", + name: "Tag", + inputType: AttributeInputTypeEnum.MULTISELECT, + valueRequired: false, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6OTA=", + name: "Security", + slug: "security", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTE=", + name: "Support", + slug: "support", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTI=", + name: "Medical", + slug: "medical", + __typename: "AttributeValue" + }, + { + id: "QXR0cmlidXRlVmFsdWU6OTM=", + name: "General", + slug: "general", + __typename: "AttributeValue" + } + ], + __typename: "Attribute" + } + ] + }, privateMetadata: [], publicationDate: "", seoDescription: "About", diff --git a/src/pages/mutations.ts b/src/pages/mutations.ts index e26d9e6fa..1b07d5025 100644 --- a/src/pages/mutations.ts +++ b/src/pages/mutations.ts @@ -1,4 +1,7 @@ -import { pageErrorFragment } from "@saleor/fragments/errors"; +import { + pageErrorFragment, + pageErrorWithAttributesFragment +} from "@saleor/fragments/errors"; import { pageDetailsFragment } from "@saleor/fragments/pages"; import gql from "graphql-tag"; @@ -17,11 +20,11 @@ import { PageUpdate, PageUpdateVariables } from "./types/PageUpdate"; const pageCreate = gql` ${pageDetailsFragment} - ${pageErrorFragment} - mutation PageCreate($input: PageInput!) { + ${pageErrorWithAttributesFragment} + mutation PageCreate($input: PageCreateInput!) { pageCreate(input: $input) { errors: pageErrors { - ...PageErrorFragment + ...PageErrorWithAttributesFragment message } page { @@ -36,11 +39,11 @@ export const TypedPageCreate = TypedMutation( const pageUpdate = gql` ${pageDetailsFragment} - ${pageErrorFragment} + ${pageErrorWithAttributesFragment} mutation PageUpdate($id: ID!, $input: PageInput!) { pageUpdate(id: $id, input: $input) { errors: pageErrors { - ...PageErrorFragment + ...PageErrorWithAttributesFragment } page { ...PageDetailsFragment diff --git a/src/pages/types/PageCreate.ts b/src/pages/types/PageCreate.ts index 4101135ea..320ba975b 100644 --- a/src/pages/types/PageCreate.ts +++ b/src/pages/types/PageCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { PageInput, PageErrorCode } from "./../../types/globalTypes"; +import { PageCreateInput, PageErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: PageCreate @@ -12,9 +12,63 @@ export interface PageCreate_pageCreate_errors { __typename: "PageError"; code: PageErrorCode; field: string | null; + attributes: string[] | null; message: string | null; } +export interface PageCreate_pageCreate_page_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageCreate_pageCreate_page_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageCreate_pageCreate_page_attributes_attribute_values | null)[] | null; +} + +export interface PageCreate_pageCreate_page_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageCreate_pageCreate_page_attributes { + __typename: "SelectedAttribute"; + attribute: PageCreate_pageCreate_page_attributes_attribute; + values: (PageCreate_pageCreate_page_attributes_values | null)[]; +} + +export interface PageCreate_pageCreate_page_pageType_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageCreate_pageCreate_page_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageCreate_pageCreate_page_pageType_attributes_values | null)[] | null; +} + +export interface PageCreate_pageCreate_page_pageType { + __typename: "PageType"; + id: string; + name: string; + attributes: (PageCreate_pageCreate_page_pageType_attributes | null)[] | null; +} + export interface PageCreate_pageCreate_page_metadata { __typename: "MetadataItem"; key: string; @@ -33,6 +87,8 @@ export interface PageCreate_pageCreate_page { title: string; slug: string; isPublished: boolean; + attributes: PageCreate_pageCreate_page_attributes[]; + pageType: PageCreate_pageCreate_page_pageType; metadata: (PageCreate_pageCreate_page_metadata | null)[]; privateMetadata: (PageCreate_pageCreate_page_privateMetadata | null)[]; contentJson: any; @@ -52,5 +108,5 @@ export interface PageCreate { } export interface PageCreateVariables { - input: PageInput; + input: PageCreateInput; } diff --git a/src/pages/types/PageDetails.ts b/src/pages/types/PageDetails.ts index d77643d92..c9fe07e7c 100644 --- a/src/pages/types/PageDetails.ts +++ b/src/pages/types/PageDetails.ts @@ -2,10 +2,65 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL query operation: PageDetails // ==================================================== +export interface PageDetails_page_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetails_page_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageDetails_page_attributes_attribute_values | null)[] | null; +} + +export interface PageDetails_page_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetails_page_attributes { + __typename: "SelectedAttribute"; + attribute: PageDetails_page_attributes_attribute; + values: (PageDetails_page_attributes_values | null)[]; +} + +export interface PageDetails_page_pageType_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageDetails_page_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageDetails_page_pageType_attributes_values | null)[] | null; +} + +export interface PageDetails_page_pageType { + __typename: "PageType"; + id: string; + name: string; + attributes: (PageDetails_page_pageType_attributes | null)[] | null; +} + export interface PageDetails_page_metadata { __typename: "MetadataItem"; key: string; @@ -24,6 +79,8 @@ export interface PageDetails_page { title: string; slug: string; isPublished: boolean; + attributes: PageDetails_page_attributes[]; + pageType: PageDetails_page_pageType; metadata: (PageDetails_page_metadata | null)[]; privateMetadata: (PageDetails_page_privateMetadata | null)[]; contentJson: any; diff --git a/src/pages/types/PageUpdate.ts b/src/pages/types/PageUpdate.ts index 3060b0b72..08b87fc55 100644 --- a/src/pages/types/PageUpdate.ts +++ b/src/pages/types/PageUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { PageInput, PageErrorCode } from "./../../types/globalTypes"; +import { PageInput, PageErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: PageUpdate @@ -12,6 +12,60 @@ export interface PageUpdate_pageUpdate_errors { __typename: "PageError"; code: PageErrorCode; field: string | null; + attributes: string[] | null; +} + +export interface PageUpdate_pageUpdate_page_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageUpdate_pageUpdate_page_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageUpdate_pageUpdate_page_attributes_attribute_values | null)[] | null; +} + +export interface PageUpdate_pageUpdate_page_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageUpdate_pageUpdate_page_attributes { + __typename: "SelectedAttribute"; + attribute: PageUpdate_pageUpdate_page_attributes_attribute; + values: (PageUpdate_pageUpdate_page_attributes_values | null)[]; +} + +export interface PageUpdate_pageUpdate_page_pageType_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface PageUpdate_pageUpdate_page_pageType_attributes { + __typename: "Attribute"; + id: string; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (PageUpdate_pageUpdate_page_pageType_attributes_values | null)[] | null; +} + +export interface PageUpdate_pageUpdate_page_pageType { + __typename: "PageType"; + id: string; + name: string; + attributes: (PageUpdate_pageUpdate_page_pageType_attributes | null)[] | null; } export interface PageUpdate_pageUpdate_page_metadata { @@ -32,6 +86,8 @@ export interface PageUpdate_pageUpdate_page { title: string; slug: string; isPublished: boolean; + attributes: PageUpdate_pageUpdate_page_attributes[]; + pageType: PageUpdate_pageUpdate_page_pageType; metadata: (PageUpdate_pageUpdate_page_metadata | null)[]; privateMetadata: (PageUpdate_pageUpdate_page_privateMetadata | null)[]; contentJson: any; diff --git a/src/pages/utils/data.ts b/src/pages/utils/data.ts new file mode 100644 index 000000000..f426eb86d --- /dev/null +++ b/src/pages/utils/data.ts @@ -0,0 +1,35 @@ +import { PageAttributeInput } from "../components/PageAttributes"; +import { + PageDetails_page, + PageDetails_page_pageType +} from "../types/PageDetails"; + +export function getAttributeInputFromPage( + page: PageDetails_page +): PageAttributeInput[] { + return page?.attributes.map(attribute => ({ + data: { + inputType: attribute.attribute.inputType, + isRequired: attribute.attribute.valueRequired, + values: attribute.attribute.values + }, + id: attribute.attribute.id, + label: attribute.attribute.name, + value: attribute.values.map(value => value.slug) + })); +} + +export function getAttributeInputFromPageType( + pageType: PageDetails_page_pageType +): PageAttributeInput[] { + return pageType?.attributes.map(attribute => ({ + data: { + inputType: attribute.inputType, + isRequired: attribute.valueRequired, + values: attribute.values + }, + id: attribute.id, + label: attribute.name, + value: [] + })); +} diff --git a/src/pages/utils/handlers.test.ts b/src/pages/utils/handlers.test.ts new file mode 100644 index 000000000..faaa40cbc --- /dev/null +++ b/src/pages/utils/handlers.test.ts @@ -0,0 +1,111 @@ +import { FormsetData } from "@saleor/hooks/useFormset"; +import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; + +import { PageAttributeInputData } from "../components/PageAttributes"; +import { createAttributeMultiChangeHandler } from "./handlers"; + +const attributes: FormsetData = [ + { + data: { + inputType: AttributeInputTypeEnum.DROPDOWN, + isRequired: false, + values: [ + { + __typename: "AttributeValue", + id: "attrv-1", + name: "Attribute 1 Value 1", + slug: "attr-1-v-1" + } + ] + }, + id: "attr-1", + label: "Attribute 1", + value: [] + }, + { + data: { + inputType: AttributeInputTypeEnum.MULTISELECT, + isRequired: false, + values: [ + { + __typename: "AttributeValue", + id: "attrv-2", + name: "Attribute 2 Value 1", + slug: "attr-2-v-1" + }, + { + __typename: "AttributeValue", + id: "attrv-3", + name: "Attribute 2 Value 2", + slug: "attr-2-v-2" + }, + { + __typename: "AttributeValue", + id: "attrv-4", + name: "Attribute 2 Value 3", + slug: "attr-2-v-3" + } + ] + }, + id: "attr-2", + label: "Attribute 2", + value: ["attr-2-v-3"] + } +]; + +describe("Multiple select", () => { + it("is able to select value", () => { + const change = jest.fn(); + const trigger = jest.fn(); + const handler = createAttributeMultiChangeHandler( + change, + attributes, + trigger + ); + + handler("attr-2", "attr-2-v-1"); + + expect(change).toHaveBeenCalledTimes(1); + expect(change.mock.calls[0][0]).toBe("attr-2"); + expect(change.mock.calls[0][1]).toHaveLength(2); + expect(change.mock.calls[0][1][0]).toBe("attr-2-v-3"); + expect(change.mock.calls[0][1][1]).toBe("attr-2-v-1"); + expect(trigger).toHaveBeenCalledTimes(1); + }); + + it("is able to deselect value", () => { + const change = jest.fn(); + const trigger = jest.fn(); + const handler = createAttributeMultiChangeHandler( + change, + attributes, + trigger + ); + + handler("attr-2", "attr-2-v-3"); + + expect(change).toHaveBeenCalledTimes(1); + expect(change.mock.calls[0][0]).toBe("attr-2"); + expect(change.mock.calls[0][1]).toHaveLength(0); + expect(trigger).toHaveBeenCalledTimes(1); + }); + + it("is able to add custom value", () => { + const change = jest.fn(); + const trigger = jest.fn(); + const handler = createAttributeMultiChangeHandler( + change, + attributes, + trigger + ); + + handler("attr-2", "A Value"); + + expect(change).toHaveBeenCalledTimes(1); + expect(change.mock.calls[0][0]).toBe("attr-2"); + expect(change.mock.calls[0][1]).toHaveLength(2); + expect(change.mock.calls[0][1][0]).toBe("attr-2-v-3"); + expect(change.mock.calls[0][1][1]).toBe("A Value"); + expect(trigger).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/pages/utils/handlers.ts b/src/pages/utils/handlers.ts new file mode 100644 index 000000000..477c940d2 --- /dev/null +++ b/src/pages/utils/handlers.ts @@ -0,0 +1,56 @@ +import { FormChange } from "@saleor/hooks/useForm"; +import { FormsetChange, FormsetData } from "@saleor/hooks/useFormset"; +import { toggle } from "@saleor/utils/lists"; + +import { PageAttributeInputData } from "../components/PageAttributes"; +import { PageDetails_page_pageType } from "../types/PageDetails"; +import { getAttributeInputFromPageType } from "./data"; + +export function createPageTypeSelectHandler( + change: FormChange, + setAttributes: (data: FormsetData) => void, + setPageType: (pageType: PageDetails_page_pageType) => void, + pageTypeChoiceList: PageDetails_page_pageType[] +): FormChange { + return (event: React.ChangeEvent) => { + const id = event.target.value; + const selectedPageType = pageTypeChoiceList.find( + pageType => pageType.id === id + ); + setPageType(selectedPageType); + change(event); + + setAttributes(getAttributeInputFromPageType(selectedPageType)); + }; +} + +export function createAttributeChangeHandler( + changeAttributeData: FormsetChange, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, value: string) => { + triggerChange(); + changeAttributeData(attributeId, value === "" ? [] : [value]); + }; +} + +export function createAttributeMultiChangeHandler( + changeAttributeData: FormsetChange, + attributes: FormsetData, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, value: string) => { + const attribute = attributes.find( + attribute => attribute.id === attributeId + ); + + const newAttributeValues = toggle( + value, + attribute.value, + (a, b) => a === b + ); + + triggerChange(); + changeAttributeData(attributeId, newAttributeValues); + }; +} diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 628c84ebf..8219263a7 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -1,6 +1,8 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; +import usePageTypeSearch from "@saleor/searches/usePageTypeSearch"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import { useMetadataUpdate, @@ -26,6 +28,14 @@ export const PageCreate: React.FC = () => { const [updateMetadata] = useMetadataUpdate({}); const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); + const { + loadMore: loadMorePageTypes, + search: searchPageTypes, + result: searchPageTypesOpts + } = usePageTypeSearch({ + variables: DEFAULT_INITIAL_SEARCH_DATA + }); + const handlePageCreate = (data: PageCreateData) => { if (data.pageCreate.errors.length === 0) { notify({ @@ -45,8 +55,13 @@ export const PageCreate: React.FC = () => { const result = await pageCreate({ variables: { input: { + attributes: formData.attributes.map(attribute => ({ + id: attribute.id, + values: attribute.value + })), contentJson: JSON.stringify(formData.content), isPublished: formData.isPublished, + pageType: formData.pageType, publicationDate: formData.publicationDate, seo: { description: formData.seoDescription, @@ -79,9 +94,18 @@ export const PageCreate: React.FC = () => { errors={pageCreateOpts.data?.pageCreate.errors || []} saveButtonBarState={pageCreateOpts.status} page={null} + pageTypes={searchPageTypesOpts.data?.search.edges.map( + edge => edge.node + )} onBack={() => navigate(pageListUrl())} onRemove={() => undefined} onSubmit={handleSubmit} + fetchPageTypes={searchPageTypes} + fetchMorePageTypes={{ + hasMore: searchPageTypesOpts.data?.search.pageInfo.hasNextPage, + loading: searchPageTypesOpts.loading, + onFetchMore: loadMorePageTypes + }} /> ); diff --git a/src/pages/views/PageDetails.tsx b/src/pages/views/PageDetails.tsx index d4fed8722..c246ba7f3 100644 --- a/src/pages/views/PageDetails.tsx +++ b/src/pages/views/PageDetails.tsx @@ -27,6 +27,10 @@ export interface PageDetailsProps { } const createPageInput = (data: PageData): PageInput => ({ + attributes: data.attributes.map(attribute => ({ + id: attribute.id, + values: attribute.value + })), contentJson: JSON.stringify(data.content), isPublished: data.isPublished, publicationDate: data.publicationDate, diff --git a/src/productTypes/components/ProductTypeAttributeUnassignDialog/ProductTypeAttributeUnassignDialog.tsx b/src/productTypes/components/ProductTypeAttributeUnassignDialog/ProductTypeAttributeUnassignDialog.tsx deleted file mode 100644 index 98306692a..000000000 --- a/src/productTypes/components/ProductTypeAttributeUnassignDialog/ProductTypeAttributeUnassignDialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; -import ActionDialog from "@saleor/components/ActionDialog"; -import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -export interface ProductTypeAttributeUnassignDialogProps { - attributeName: string; - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - productTypeName: string; - onClose: () => void; - onConfirm: () => void; -} - -const ProductTypeAttributeUnassignDialog: React.FC = ({ - attributeName, - confirmButtonState, - open, - productTypeName, - onClose, - onConfirm -}) => { - const intl = useIntl(); - - return ( - - - {attributeName}, - productTypeName: {productTypeName} - }} - /> - - - ); -}; -ProductTypeAttributeUnassignDialog.displayName = - "ProductTypeAttributeUnassignDialog"; -export default ProductTypeAttributeUnassignDialog; diff --git a/src/productTypes/components/ProductTypeAttributeUnassignDialog/index.ts b/src/productTypes/components/ProductTypeAttributeUnassignDialog/index.ts deleted file mode 100644 index 35eade813..000000000 --- a/src/productTypes/components/ProductTypeAttributeUnassignDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ProductTypeAttributeUnassignDialog"; -export * from "./ProductTypeAttributeUnassignDialog"; diff --git a/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx b/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx index dfd6d57d3..142bb90b7 100644 --- a/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx +++ b/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx @@ -16,7 +16,7 @@ import { import TableHead from "@saleor/components/TableHead"; import { maybe, renderCollection, stopPropagation } from "@saleor/misc"; import { ListActions, ReorderAction } from "@saleor/types"; -import { AttributeTypeEnum } from "@saleor/types/globalTypes"; +import { ProductAttributeType } from "@saleor/types/globalTypes"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -56,7 +56,7 @@ interface ProductTypeAttributesProps extends ListActions { | ProductTypeDetails_productType_variantAttributes[]; disabled: boolean; type: string; - onAttributeAssign: (type: AttributeTypeEnum) => void; + onAttributeAssign: (type: ProductAttributeType) => void; onAttributeClick: (id: string) => void; onAttributeReorder: ReorderAction; onAttributeUnassign: (id: string) => void; @@ -87,14 +87,14 @@ const ProductTypeAttributes: React.FC = props => { return ( = props => { + + +
+
+
+ + Private Metadata + +
+
+
+
+ +
+
+
+ +
+`; + +exports[`Storyshots Views / Page types / Create page type form errors 1`] = ` +
+
+
+
+
+ Create Page Type +
+
+
+
+
+
+
+
+ General Information +
+
+ These are general information about this Content Type. +
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+

+ This field is required +

+
+
+
+
+
+
+ Metadata +
+
+
+
+ + Metadata + +
+
+
+
+ +
+
+
+ + Private Metadata + +
+
+
+
+ +
+
+
+ +
+`; + +exports[`Storyshots Views / Page types / Create page type loading 1`] = ` +
+
+
+
+
+ Create Page Type +
+
+
+
+
+
+
+
+ General Information +
+
+ These are general information about this Content Type. +
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ Metadata +
+
+
+
+ + Metadata + +
+
+
+
+ +
+
+
+ + Private Metadata + +
+
+
+
+ +
+
+
+ +
+`; + +exports[`Storyshots Views / Page types / Page type details default 1`] = ` +
+
+
+
+
+ Blog +
+
+
+
+
+
+
+
+ General Information +
+
+ These are general information about this Content Type. +
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ Content Attributes +
+
+ This list shows all attributes that will be assigned to pages that have this page type assigned. +
+
+
+
+ + Content Attributes + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + Attribute name + + Slug + +
+
+
+
+
+
+ Metadata +
+
+
+
+ + Metadata + +
+
+
+
+ + + + + + + + + + + + +