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
This commit is contained in:
Dawid Tarasiuk 2020-11-19 15:42:14 +01:00 committed by GitHub
parent 080f6e5b01
commit fc02fce701
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 11407 additions and 972 deletions

View file

@ -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

View file

@ -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 wont 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 wont 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 wont be able to revert changes.} other{Are you sure you want to delete {displayQuantity} page types? After doing so you wont 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 wont 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 wont 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",

41
package-lock.json generated
View file

@ -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
}
}
},

View file

@ -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

View file

@ -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<any>) => void;
}
@ -66,7 +67,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
})}
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<AttributeDetailsProps> = ({
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"

View file

@ -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<any>) => 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<AttributeOrganizationProps> = props => {
const { canChangeType, data, disabled, onChange } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Organization",
description: "section header"
})}
/>
<CardContent>
{canChangeType ? (
<RadioGroupField
choices={[
{
label: intl.formatMessage(messages.productAttribute),
value: AttributeTypeEnum.PRODUCT_TYPE
},
{
label: intl.formatMessage(messages.contentAttribute),
value: AttributeTypeEnum.PAGE_TYPE
}
]}
disabled={disabled}
label={
<>
<FormattedMessage defaultMessage="Attribute Class" />
<Typography variant="caption">
<FormattedMessage defaultMessage="Define where this attribute should be used in Saleor system" />
</Typography>
</>
}
name={"type" as keyof FormData}
value={data.type}
onChange={onChange}
/>
) : (
<>
<Typography className={classes.label} variant="caption">
<FormattedMessage defaultMessage="Attribute Class" />
</Typography>
<Typography>
{data.type === AttributeTypeEnum.PRODUCT_TYPE
? intl.formatMessage(messages.productAttribute)
: intl.formatMessage(messages.contentAttribute)}
</Typography>
</>
)}
</CardContent>
</Card>
);
};
AttributeOrganization.displayName = "AttributeOrganization";
export default AttributeOrganization;

View file

@ -0,0 +1,2 @@
export { default } from "./AttributeOrganization";
export * from "./AttributeOrganization";

View file

@ -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<AttributePageProps> = ({
privateMetadata: [],
slug: "",
storefrontSearchPosition: "",
type: AttributeTypeEnum.PRODUCT_TYPE,
valueRequired: true,
visibleInStorefront: true
}
@ -114,6 +120,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
() => 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<AttributePageProps> = ({
!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<AttributePageProps> = ({
<Metadata data={data} onChange={changeMetadata} />
</div>
<div>
<AttributeOrganization
canChangeType={attribute === null}
data={data}
disabled={disabled}
onChange={change}
/>
<CardSpacer />
<AttributeProperties
data={data}
errors={errors}

View file

@ -5,11 +5,14 @@ import Typography from "@material-ui/core/Typography";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { AttributeTypeEnum } from "@saleor/types/globalTypes";
import { getFormErrors } from "@saleor/utils/errors";
import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -18,7 +21,7 @@ import { AttributePageFormData } from "../AttributePage";
export interface AttributePropertiesProps {
data: AttributePageFormData;
disabled: boolean;
errors: ProductErrorFragment[];
errors: AttributeErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
}
@ -74,6 +77,8 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
/>
</Typography>
<Hr />
{data.type === AttributeTypeEnum.PRODUCT_TYPE && (
<>
<ControlledCheckbox
name={"filterableInStorefront" as keyof FormData}
label={intl.formatMessage({
@ -85,12 +90,15 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
disabled={disabled}
/>
<FormSpacer />
{data.filterableInStorefront && (
</>
)}
{data.filterableInStorefront &&
data.type === AttributeTypeEnum.PRODUCT_TYPE && (
<TextField
disabled={disabled}
error={!!formErrors.storefrontSearchPosition}
fullWidth
helperText={getProductErrorMessage(
helperText={getAttributeErrorMessage(
formErrors.storefrontSearchPosition,
intl
)}
@ -104,12 +112,19 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
/>
)}
<FormSpacer />
<ControlledCheckbox
<ControlledSwitch
name={"visibleInStorefront" as keyof FormData}
label={intl.formatMessage({
defaultMessage: "Visible on Product Page in Storefront",
description: "attribute"
})}
label={
<>
<FormattedMessage
defaultMessage="Public"
description="attribute visibility in storefront"
/>
<Typography variant="caption">
<FormattedMessage defaultMessage="If enabled, attribute will be accessible to customers." />
</Typography>
</>
}
checked={data.visibleInStorefront}
onChange={onChange}
disabled={disabled}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
name: "Author",
slug: "author",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -86,6 +89,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo2",
name: "Box Size",
slug: "box-size",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -135,6 +139,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZToz",
name: "Brand",
slug: "brand",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -157,6 +162,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo4",
name: "Candy Box Size",
slug: "candy-box-size",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -197,6 +203,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo1",
name: "Coffee Genre",
slug: "coffee-genre",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -228,6 +235,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZToy",
name: "Collar",
slug: "collar",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -268,6 +276,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTox",
name: "Color",
slug: "color",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -299,6 +308,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZToxMg==",
name: "Cover",
slug: "cover",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -366,6 +376,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo3",
name: "Flavor",
slug: "flavor",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -397,6 +408,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==",
name: "Language",
slug: "language",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -428,6 +440,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZToxMA==",
name: "Publisher",
slug: "publisher",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
@ -459,6 +472,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
id: "UHJvZHVjdEF0dHJpYnV0ZTo0",
name: "Size",
slug: "size",
type: AttributeTypeEnum.PRODUCT_TYPE,
values: [
{
__typename: "AttributeValue" as "AttributeValue",

View file

@ -1,5 +1,5 @@
import { attributeDetailsFragment } from "@saleor/fragments/attributes";
import { productErrorFragment } from "@saleor/fragments/errors";
import { attributeErrorFragment } from "@saleor/fragments/errors";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag";
@ -37,11 +37,11 @@ import {
} from "./types/AttributeValueUpdate";
const attributeBulkDelete = gql`
${productErrorFragment}
${attributeErrorFragment}
mutation AttributeBulkDelete($ids: [ID!]!) {
attributeBulkDelete(ids: $ids) {
errors: productErrors {
...ProductErrorFragment
errors: attributeErrors {
...AttributeErrorFragment
}
}
}
@ -52,11 +52,11 @@ export const useAttributeBulkDeleteMutation = makeMutation<
>(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
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<AttributeDetailsProps> = ({ params }) => {
const [values, setValues] = React.useState<
AttributeValueEditDialogFormData[]
>([]);
const [valueErrors, setValueErrors] = React.useState<ProductErrorFragment[]>(
[]
);
const [valueErrors, setValueErrors] = React.useState<
AttributeErrorFragment[]
>([]);
const [attributeCreate, attributeCreateOpts] = useAttributeCreateMutation({
onCompleted: data => {

View file

@ -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<AttributeDetailsProps> = ({ id, params }) => {
if (data.attributeReorderValues.errors.length !== 0) {
notify({
status: "error",
text: getProductErrorMessage(
text: getAttributeErrorMessage(
data.attributeReorderValues.errors[0],
intl
)

View file

@ -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,

View file

@ -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;

View file

@ -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", () => <AttributeUnassignDialog {...props} />);

View file

@ -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<AttributeUnassignDialogProps> = ({
title,
attributeName,
confirmButtonState,
open,
itemTypeName,
onClose,
onConfirm
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={title}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to unassign {attributeName} from {itemTypeName}?"
values={{
attributeName: <strong>{attributeName}</strong>,
itemTypeName: <strong>{itemTypeName}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
);
AttributeUnassignDialog.displayName = "AttributeUnassignDialog";
export default AttributeUnassignDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./AttributeUnassignDialog";
export * from "./AttributeUnassignDialog";

View file

@ -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", () => <BulkAttributeUnassignDialog {...props} />);

View file

@ -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<BulkAttributeUnassignDialogProps> = ({
title,
attributeQuantity,
confirmButtonState,
open,
itemTypeName,
onClose,
onConfirm
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={title}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{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}?}}"
description="unassign multiple attributes from item"
values={{
attributeQuantity: <strong>{attributeQuantity}</strong>,
counter: attributeQuantity,
itemTypeName: <strong>{itemTypeName}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
);
BulkAttributeUnassignDialog.displayName = "BulkAttributeUnassignDialog";
export default BulkAttributeUnassignDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./BulkAttributeUnassignDialog";
export * from "./BulkAttributeUnassignDialog";

View file

@ -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<any>) => void;

View file

@ -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: <PageTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PAGES,
title: intl.formatMessage(sectionNames.pageTypes),
url: pageTypeListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "Manage and add additional pages",
id: "configurationMenuPages"
}),
icon: <Pages fontSize="inherit" viewBox="0 0 44 44" />,
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: <Pages fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PAGES,
title: intl.formatMessage(sectionNames.pages),
url: pageListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "View and update your plugins and their settings.",

View file

@ -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
}
`;

View file

@ -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

View file

@ -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
}
}
`;

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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)[];
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

16
src/icons/PageTypes.tsx Normal file
View file

@ -0,0 +1,16 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const PageTypes = createSvgIcon(
<>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M19 0H0V40H26.4056H27.6667V8L19 0ZM25.1445 9.00369V37.5769H2.52221V2.42311H17.9553L18 2.46406V9H25.1404L25.1445 9.00369ZM5 12H23V17H5V12ZM12 20H5V30H12V20ZM15 20H23V35H15V20ZM12 33H5V35H12V33Z"
fill="#06847B"
/>
</>,
"PageTypes"
);
export default PageTypes;

View file

@ -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}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PAGES]}
path="/page-types"
component={PageTypesSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins"

View file

@ -215,6 +215,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Orders",
description: "orders section name"
},
pageTypes: {
defaultMessage: "Page Types",
description: "page types section name"
},
pages: {
defaultMessage: "Pages",
description: "pages section name"

View file

@ -0,0 +1,191 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import TableHead from "@saleor/components/TableHead";
import { renderCollection, stopPropagation } from "@saleor/misc";
import { PageTypeDetails_pageType_attributes } from "@saleor/pageTypes/types/PageTypeDetails";
import { ListActions, ReorderAction } from "@saleor/types";
import { AttributeTypeEnum } from "@saleor/types/globalTypes";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
const useStyles = makeStyles(
{
colAction: {
"&:last-child": {
paddingRight: 0
},
width: 80
},
colGrab: {
width: 60
},
colName: {},
colSlug: {
width: 300
},
link: {
cursor: "pointer"
},
textLeft: {
textAlign: "left"
}
},
{ name: "PageTypeAttributes" }
);
interface PageTypeAttributesProps extends ListActions {
attributes: PageTypeDetails_pageType_attributes[];
disabled: boolean;
type: string;
onAttributeAssign: (type: AttributeTypeEnum) => void;
onAttributeClick: (id: string) => void;
onAttributeReorder: ReorderAction;
onAttributeUnassign: (id: string) => void;
}
const numberOfColumns = 5;
const PageTypeAttributes: React.FC<PageTypeAttributesProps> = props => {
const {
attributes,
disabled,
isChecked,
selected,
toggle,
toggleAll,
toolbar,
type,
onAttributeAssign,
onAttributeClick,
onAttributeReorder,
onAttributeUnassign
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card data-test="page-attributes">
<CardTitle
title={intl.formatMessage({
defaultMessage: "Content Attributes",
description: "section header"
})}
toolbar={
<Button
color="primary"
variant="text"
onClick={() => onAttributeAssign(AttributeTypeEnum[type])}
>
<FormattedMessage
defaultMessage="Assign attribute"
description="button"
/>
</Button>
}
/>
<ResponsiveTable>
<colgroup>
<col className={classes.colGrab} />
<col />
<col className={classes.colName} />
<col className={classes.colSlug} />
<col className={classes.colAction} />
</colgroup>
{attributes?.length > 0 && (
<TableHead
colSpan={numberOfColumns}
disabled={disabled}
dragRows
selected={selected}
items={attributes}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Attribute name" />
</TableCell>
<TableCell className={classes.colName}>
<FormattedMessage
defaultMessage="Slug"
description="attribute internal name"
/>
</TableCell>
<TableCell />
</TableHead>
)}
<SortableTableBody onSortEnd={onAttributeReorder}>
{renderCollection(
attributes,
(attribute, attributeIndex) => {
const isSelected = attribute ? isChecked(attribute.id) : false;
return (
<SortableTableRow
selected={isSelected}
className={!!attribute ? classes.link : undefined}
hover={!!attribute}
onClick={
!!attribute
? () => onAttributeClick(attribute.id)
: undefined
}
key={attribute?.id}
index={attributeIndex || 0}
data-test="id"
data-test-id={attribute?.id}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(attribute.id)}
/>
</TableCell>
<TableCell className={classes.colName} data-test="name">
{attribute?.name || <Skeleton />}
</TableCell>
<TableCell className={classes.colSlug} data-test="slug">
{attribute?.slug || <Skeleton />}
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
onClick={stopPropagation(() =>
onAttributeUnassign(attribute.id)
)}
>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</SortableTableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No attributes found" />
</TableCell>
</TableRow>
)
)}
</SortableTableBody>
</ResponsiveTable>
</Card>
);
};
PageTypeAttributes.displayName = "PageTypeAttributes";
export default PageTypeAttributes;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeAttributes";
export * from "./PageTypeAttributes";

View file

@ -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<PageTypeBulkDeleteDialogProps> = ({
confirmButtonState,
open,
quantity,
hasPages,
onClose,
onConfirm
}) => {
const intl = useIntl();
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
defaultMessage: "Delete Page Types",
description: "dialog header"
})}
variant="delete"
>
<DialogContentText>
{hasPages ? (
<FormattedMessage
defaultMessage="{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 wont 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 wont be able to revert changes.}}"
description="delete page types with its pages"
values={{
counter: quantity,
displayQuantity: <strong>{quantity}</strong>
}}
/>
) : (
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this page type? After doing so you wont be able to revert changes.} other{Are you sure you want to delete {displayQuantity} page types? After doing so you wont be able to revert changes.}}"
description="delete page types"
values={{
counter: quantity,
displayQuantity: <strong>{quantity}</strong>
}}
/>
)}
</DialogContentText>
</ActionDialog>
);
};
PageTypeBulkDeleteDialog.displayName = "PageTypeBulkDeleteDialog";
export default PageTypeBulkDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeBulkDeleteDialog";
export * from "./PageTypeBulkDeleteDialog";

View file

@ -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<PageTypeCreatePageProps, "classes"> = {
disabled: false,
errors: [],
onBack: () => undefined,
onSubmit: () => undefined,
saveButtonBarState: "default"
};
storiesOf("Views / Page types / Create page type", module)
.addDecorator(Decorator)
.add("default", () => <PageTypeCreatePage {...props} />)
.add("loading", () => <PageTypeCreatePage {...props} disabled={true} />)
.add("form errors", () => (
<PageTypeCreatePage
{...props}
errors={[
{
code: PageErrorCode.REQUIRED,
field: "name"
}
].map(err => ({
__typename: "PageError",
...err
}))}
/>
));

View file

@ -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<PageTypeCreatePageProps> = props => {
const { disabled, errors, saveButtonBarState, onBack, onSubmit } = props;
const classes = useStyles(props);
const intl = useIntl();
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
return (
<Form initial={formInitialData} onSubmit={onSubmit} confirmLeave>
{({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.pageTypes)}
</AppHeader>
<PageHeader
title={intl.formatMessage({
defaultMessage: "Create Page Type",
description: "header"
})}
/>
<Grid variant="inverted">
<div>
<Typography>
{intl.formatMessage(commonMessages.generalInformations)}
</Typography>
<Typography variant="body2">
<FormattedMessage defaultMessage="These are general information about this Content Type." />
</Typography>
</div>
<PageTypeDetails
data={data}
disabled={disabled}
errors={errors}
onChange={change}
/>
<Hr className={classes.hr} />
<div>
<Typography>
<FormattedMessage
defaultMessage="Metadata"
description="section header"
/>
</Typography>
</div>
<Metadata data={data} onChange={changeMetadata} />
<div></div>
</Grid>
<SaveButtonBar
onCancel={onBack}
onSave={submit}
disabled={disabled || !hasChanged}
state={saveButtonBarState}
/>
</Container>
);
}}
</Form>
);
};
PageTypeCreatePage.displayName = "PageTypeCreatePage";
export default PageTypeCreatePage;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeCreatePage";
export * from "./PageTypeCreatePage";

View file

@ -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<PageTypeDeleteDialogProps> = ({
confirmButtonState,
open,
name,
hasPages,
onClose,
onConfirm
}) => {
const intl = useIntl();
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
defaultMessage: "Delete Page Type",
description: "dialog header"
})}
variant="delete"
>
<DialogContentText>
{hasPages ? (
<FormattedMessage
defaultMessage="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 wont be able to revert changes."
description="delete page type with its pages"
values={{
name: <strong>{name}</strong>
}}
/>
) : (
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}? After doing so you wont be able to revert changes."
description="delete page type"
values={{
name: <strong>{name}</strong>
}}
/>
)}
</DialogContentText>
</ActionDialog>
);
};
PageTypeDeleteDialog.displayName = "PageTypeDeleteDialog";
export default PageTypeDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeDeleteDialog";
export * from "./PageTypeDeleteDialog";

View file

@ -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<any>) => void;
}
const PageTypeDetails: React.FC<PageTypeDetailsProps> = props => {
const { data, disabled, errors, onChange } = props;
const intl = useIntl();
const formErrors = getFormErrors(["name"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage(commonMessages.generalInformations)}
/>
<CardContent>
<TextField
disabled={disabled}
fullWidth
error={!!formErrors.name}
helperText={getPageErrorMessage(formErrors.name, intl)}
label={intl.formatMessage({
defaultMessage: "Content Type Name"
})}
name="name"
onChange={onChange}
value={data.name}
/>
</CardContent>
</Card>
);
};
PageTypeDetails.defaultProps = {
errors: []
};
PageTypeDetails.displayName = "PageTypeDetails";
export default PageTypeDetails;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeDetails";
export * from "./PageTypeDetails";

View file

@ -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<PageTypeDetailsPageProps, "classes"> = {
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", () => <PageTypeDetailsPage {...props} />)
.add("loading", () => (
<PageTypeDetailsPage
{...props}
disabled={true}
pageTitle={undefined}
pageType={undefined}
/>
))
.add("no attributes", () => (
<PageTypeDetailsPage
{...props}
pageType={{
...pageType,
attributes: []
}}
/>
))
.add("form errors", () => (
<PageTypeDetailsPage
{...props}
errors={[
{
code: PageErrorCode.REQUIRED,
field: "name"
}
].map(err => ({
__typename: "PageError",
...err
}))}
/>
));

View file

@ -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<PageTypeDetailsPageProps> = 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 (
<Form initial={formInitialData} onSubmit={handleSubmit} confirmLeave>
{({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.pageTypes)}
</AppHeader>
<PageHeader title={pageTitle} />
<Grid variant="inverted">
<div>
<Typography>
{intl.formatMessage(commonMessages.generalInformations)}
</Typography>
<Typography variant="body2">
<FormattedMessage defaultMessage="These are general information about this Content Type." />
</Typography>
</div>
<PageTypeDetails
data={data}
disabled={disabled}
errors={errors}
onChange={change}
/>
<Hr className={classes.hr} />
<div>
<Typography>
<FormattedMessage
defaultMessage="Content Attributes"
description="section header"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage defaultMessage="This list shows all attributes that will be assigned to pages that have this page type assigned." />
</Typography>
</div>
<PageTypeAttributes
attributes={pageType?.attributes}
disabled={disabled}
type={AttributeTypeEnum.PAGE_TYPE}
onAttributeAssign={onAttributeAdd}
onAttributeClick={onAttributeClick}
onAttributeReorder={(event: ReorderEvent) =>
onAttributeReorder(event, AttributeTypeEnum.PAGE_TYPE)
}
onAttributeUnassign={onAttributeUnassign}
{...attributeList}
/>
<Hr className={classes.hr} />
<div>
<Typography>
<FormattedMessage
defaultMessage="Metadata"
description="section header"
/>
</Typography>
</div>
<Metadata data={data} onChange={changeMetadata} />
</Grid>
<SaveButtonBar
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
disabled={disabled || !hasChanged}
state={saveButtonBarState}
/>
</Container>
);
}}
</Form>
);
};
PageTypeDetailsPage.displayName = "PageTypeDetailsPage";
export default PageTypeDetailsPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeDetailsPage";
export * from "./PageTypeDetailsPage";

View file

@ -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<PageTypeListUrlSortField> {
pageTypes: PageTypeList_pageTypes_edges_node[];
}
const numberOfColumns = 2;
const PageTypeList: React.FC<PageTypeListProps> = props => {
const {
disabled,
pageTypes,
pageInfo,
onNextPage,
onPreviousPage,
onRowClick,
onSort,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar
} = props;
const classes = useStyles(props);
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={pageTypes}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
direction={
sort.sort === PageTypeListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(PageTypeListUrlSortField.name)}
className={classes.colName}
>
<FormattedMessage
defaultMessage="Content Type Name"
description="page type name"
/>
</TableCellHeader>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
pageTypes,
pageType => {
const isSelected = pageType ? isChecked(pageType.id) : false;
return (
<TableRow
className={!!pageType ? classes.link : undefined}
hover={!!pageType}
key={pageType ? pageType.id : "skeleton"}
onClick={pageType ? onRowClick(pageType.id) : undefined}
selected={isSelected}
data-test="id"
data-test-id={pageType?.id}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(pageType.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
{pageType ? (
<span data-test="name">{pageType.name}</span>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No page types found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
);
};
PageTypeList.displayName = "PageTypeList";
export default PageTypeList;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeList";
export * from "./PageTypeList";

View file

@ -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", () => <PageTypeListPage {...props} />)
.add("loading", () => (
<PageTypeListPage {...props} disabled={true} pageTypes={undefined} />
))
.add("no data", () => <PageTypeListPage {...props} pageTypes={[]} />);

View file

@ -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<PageTypeListUrlSortField>,
TabPageProps {
pageTypes: PageTypeList_pageTypes_edges_node[];
onBack: () => void;
}
const PageTypeListPage: React.FC<PageTypeListPageProps> = ({
currentTab,
initialSearch,
onAdd,
onAll,
onBack,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
...listProps
}) => {
const intl = useIntl();
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.configuration)}
</AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.pageTypes)}>
<Button color="primary" variant="contained" onClick={onAdd}>
<FormattedMessage
defaultMessage="create page type"
description="button"
/>
</Button>
</PageHeader>
<Card>
<SearchBar
allTabLabel={intl.formatMessage({
defaultMessage: "All Page Types",
description: "tab name"
})}
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Page Type"
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<PageTypeList {...listProps} />
</Card>
</Container>
);
};
PageTypeListPage.displayName = "PageTypeListPage";
export default PageTypeListPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeListPage";
export * from "./PageTypeListPage";

73
src/pageTypes/fixtures.ts Normal file
View file

@ -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: []
};

View file

@ -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
}
)
);

View file

@ -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;
}

62
src/pageTypes/index.tsx Normal file
View file

@ -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<RouteComponentProps<{}>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: PageTypeListUrlQueryParams = asSortParams(
qs,
PageTypeListUrlSortField
);
return <PageTypeListComponent params={params} />;
};
interface PageTypeDetailsRouteParams {
id: string;
}
const PageTypeDetails: React.FC<RouteComponentProps<
PageTypeDetailsRouteParams
>> = ({ match }) => {
const qs = parseQs(location.search.substr(1));
const params: PageTypeUrlQueryParams = qs;
return (
<PageTypeDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
export const PageTypeRouter: React.FC = () => {
const intl = useIntl();
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.pageTypes)} />
<Switch>
<Route exact path={pageTypeListPath} component={PageTypeList} />
<Route exact path={pageTypeAddPath} component={PageTypeCreate} />
<Route path={pageTypePath(":id")} component={PageTypeDetails} />
</Switch>
</>
);
};
PageTypeRouter.displayName = "PageTypeRouter";
export default PageTypeRouter;

161
src/pageTypes/mutations.ts Normal file
View file

@ -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);

63
src/pageTypes/queries.ts Normal file
View file

@ -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);

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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[];
}

52
src/pageTypes/urls.ts Normal file
View file

@ -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<PageTypeListUrlFiltersEnum>;
export type PageTypeListUrlDialog = "remove" | TabActionDialog;
export enum PageTypeListUrlSortField {
name = "name"
}
export type PageTypeListUrlSort = Sort<PageTypeListUrlSortField>;
export type PageTypeListUrlQueryParams = ActiveTab &
BulkAction &
Dialog<PageTypeListUrlDialog> &
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<PageTypeUrlDialog> &
SingleAction & {
type?: string;
};
export const pageTypeUrl = (id: string, params?: PageTypeUrlQueryParams) =>
pageTypePath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -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 (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Page Type",
description: "window title",
id: "pageTypeCreateHeader"
})}
/>
<PageTypeCreatePage
disabled={createPageTypeOpts.loading}
errors={createPageTypeOpts.data?.pageTypeCreate.errors || []}
saveButtonBarState={createPageTypeOpts.status}
onBack={() => navigate(pageTypeListUrl())}
onSubmit={handleSubmit}
/>
</>
);
};
export default PageTypeCreate;

View file

@ -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<PageTypeDetailsProps> = ({
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 <NotFoundPage onBack={handleBack} />;
}
const closeModal = () => navigate(pageTypeUrl(id), true);
const handleSubmit = createMetadataUpdateHandler(
data?.pageType,
handlePageTypeUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const loading = updatePageTypeOpts.loading || dataLoading;
return (
<>
<WindowTitle title={data?.pageType.name} />
<PageTypeDetailsPage
disabled={loading}
errors={updatePageTypeOpts.data?.pageTypeUpdate.errors}
pageTitle={data?.pageType.name}
pageType={data?.pageType}
saveButtonBarState={updatePageTypeOpts.status}
onAttributeAdd={type =>
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: (
<Button
color="primary"
onClick={() =>
navigate(
pageTypeUrl(id, {
action: "unassign-attributes",
ids: attributeListActions.listElements
})
)
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign attribute from page type, button"
/>
</Button>
)
}}
/>
<PageTypeDeleteDialog
confirmButtonState={deletePageTypeOpts.status}
name={getStringOrPlaceholder(data?.pageType.name)}
hasPages={data?.pageType.hasPages}
open={params.action === "remove"}
onClose={() => navigate(pageTypeUrl(id))}
onConfirm={handlePageTypeDelete}
/>
{!dataLoading && (
<AssignAttributeDialog
attributes={result.data?.pageType.availableAttributes.edges.map(
edge => 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]
})
);
}}
/>
)}
<BulkAttributeUnassignDialog
title={intl.formatMessage({
defaultMessage: "Unassign Attribute from Page Type",
description: "dialog header"
})}
attributeQuantity={params.ids?.length}
confirmButtonState={unassignAttributeOpts.status}
onClose={closeModal}
onConfirm={handleBulkAttributeUnassign}
open={params.action === "unassign-attributes"}
itemTypeName={getStringOrPlaceholder(data?.pageType.name)}
/>
<AttributeUnassignDialog
title={intl.formatMessage({
defaultMessage: "Unassign Attribute From Page Type",
description: "dialog header"
})}
attributeName={getStringOrPlaceholder(
data?.pageType.attributes.find(
attribute => 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;

View file

@ -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<PageTypeListProps> = ({ 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 (
<>
<PageTypeListPage
currentTab={currentTab}
initialSearch={params.query || ""}
onSearchChange={query => 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={
<IconButton
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
/>
<PageTypeBulkDeleteDialog
confirmButtonState={pageTypeBulkDeleteOpts.status}
quantity={params.ids?.length}
hasPages={selectedPageTypesHasPages}
open={params.action === "remove"}
onClose={closeModal}
onConfirm={hanldePageTypeBulkDelete}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
/>
</>
);
};
PageTypeList.displayName = "PageTypeList";
export default PageTypeList;

View file

@ -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<PageTypeListUrlFilters>(PAGE_TYPE_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
PageTypeListUrlQueryParams,
PageTypeListUrlFilters
>(PageTypeListUrlFiltersEnum);

View file

@ -0,0 +1,2 @@
export { default } from "./PageTypeList";
export * from "./PageTypeList";

View file

@ -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
);

View file

@ -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<PageAttributesProps> = ({
attributes,
disabled,
errors,
onChange,
onMultiChange
}) => {
const intl = useIntl();
const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded);
return (
<Card className={classes.card}>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Attributes",
description: "page attributes, section header"
})}
/>
<CardContent className={classes.cardContent}>
<div className={classes.expansionBar}>
<div className={classes.expansionBarLabelContainer}>
<Typography className={classes.expansionBarLabel} variant="caption">
<FormattedMessage
defaultMessage="{number} Attributes"
description="number of page attributes"
values={{
number: attributes.length
}}
/>
</Typography>
</div>
<IconButton
className={classes.expansionBarButton}
onClick={toggleExpansion}
data-test="page-attributes-expand"
>
<ArrowDropDownIcon
className={classNames(classes.expansionBarButtonIcon, {
[classes.rotate]: expanded
})}
/>
</IconButton>
</div>
{expanded && attributes.length > 0 && (
<>
<Hr />
{attributes.map((attribute, attributeIndex) => {
const error = errors.find(err =>
err.attributes?.includes(attribute.id)
);
return (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test="page-attribute-label"
>
<Typography>{attribute.label}</Typography>
</div>
<div data-test="page-attribute-value">
{attribute.data.inputType ===
AttributeInputTypeEnum.DROPDOWN ? (
<SingleAutocompleteSelectField
choices={getSingleChoices(attribute.data.values)}
disabled={disabled}
displayValue={
attribute.data.values.find(
value => 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}
/>
) : (
<MultiAutocompleteSelectField
choices={getMultiChoices(attribute.data.values)}
displayValues={getMultiDisplayValue(attribute)}
error={!!error}
helperText={getPageErrorMessage(error, intl)}
label={intl.formatMessage({
defaultMessage: "Values",
description: "attribute values"
})}
name={`attribute:${attribute.label}`}
value={attribute.value}
onChange={event =>
onMultiChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
)}
</div>
</Grid>
</React.Fragment>
);
})}
</>
)}
</CardContent>
</Card>
);
};
PageAttributes.displayName = "PageAttributes";
export default PageAttributes;

View file

@ -0,0 +1,2 @@
export { default } from "./PageAttributes";
export * from "./PageAttributes";

View file

@ -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<PageDetailsPageProps> = ({
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<PageDetailsPageProps> = ({
const pageExists = page !== null;
return (
<PageForm page={page} onSubmit={onSubmit}>
{({ change, data, handlers, hasChanged, submit }) => (
<PageForm page={page} pageTypes={pageTypes} onSubmit={onSubmit}>
{({ change, data, pageType, handlers, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.pages)}
@ -88,6 +98,16 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
})}
/>
<CardSpacer />
{data.attributes.length > 0 && (
<PageAttributes
attributes={data.attributes}
disabled={disabled}
errors={errors}
onChange={handlers.changeAttribute}
onMultiChange={handlers.changeAttributeMulti}
/>
)}
<CardSpacer />
<Metadata data={data} onChange={handlers.changeMetadata} />
</div>
<div>
@ -117,6 +137,19 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
}}
onChange={change}
/>
<CardSpacer />
<PageOrganizeContent
data={data}
errors={errors}
disabled={disabled}
pageTypes={pageTypes}
pageType={pageType}
pageTypeInputDisplayValue={pageType?.name || ""}
onPageTypeChange={handlers.selectPageType}
fetchPageTypes={fetchPageTypes}
fetchMorePageTypes={fetchMorePageTypes}
canChangeType={!page?.pageType}
/>
</div>
</Grid>
<SaveButtonBar

View file

@ -1,8 +1,20 @@
import { OutputData } from "@editorjs/editorjs";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import { PageTypeFragment } from "@saleor/fragments/types/PageTypeFragment";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import { PageDetails_page } from "@saleor/pages/types/PageDetails";
import useFormset, { FormsetChange } from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import {
PageDetails_page,
PageDetails_page_pageType
} from "@saleor/pages/types/PageDetails";
import { getAttributeInputFromPage } from "@saleor/pages/utils/data";
import { createPageTypeSelectHandler } from "@saleor/pages/utils/handlers";
import {
createAttributeChangeHandler,
createAttributeMultiChangeHandler
} from "@saleor/products/utils/handlers";
import getPublicationData from "@saleor/utils/data/getPublicationData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
@ -11,6 +23,8 @@ import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTr
import useRichText from "@saleor/utils/richText/useRichText";
import React from "react";
import { PageAttributeInput, PageAttributeInputData } from "../PageAttributes";
export interface PageFormData extends MetadataFormData {
isPublished: boolean;
publicationDate: string;
@ -18,18 +32,24 @@ export interface PageFormData extends MetadataFormData {
seoTitle: string;
slug: string;
title: string;
pageType: string;
}
export interface PageData extends PageFormData {
attributes: PageAttributeInput[];
content: OutputData;
}
interface PageUpdateHandlers {
changeMetadata: FormChange;
changeContent: RichTextEditorChange;
selectPageType: FormChange;
changeAttribute: FormsetChange<string>;
changeAttributeMulti: FormsetChange<string>;
}
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<PageAttributeInputData>(attributesFromPage || []);
const form = useForm<PageFormData>({
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<PageTypeFragment>(
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<PageFormProps> = ({ children, page, onSubmit }) => {
const props = usePageForm(page, onSubmit);
const PageForm: React.FC<PageFormProps> = ({
children,
page,
pageTypes,
onSubmit
}) => {
const props = usePageForm(page, onSubmit, pageTypes);
return <form onSubmit={props.submit}>{children(props)}</form>;
};

Some files were not shown because too many files have changed in this diff Show more