Add product export (#620)
* Add component backbone * Make step component generic and typed * Add step tabs * Add settings view * Encapsulate all dialog components in one directory * Move types to separate file * Add mutations * Use gql types * Add error handling * Do not keep separate types file * Allow products to be exported * Fix types * Update snapshots * Update to latest schema * Add disabled option * Use wizard hook * Update type definitions * Queue export check task * Fix bug causing jobs to be endless and duplicated * Fix minor bugs * Add accordion component * Allow selection of fields to be exported * Add attribute export * Update snapshots * Update messages * Update changelog * Add missing key * Add quick peek to accordioin * Sort imports * Remove unused files * Add chiips to attribute selection * Change menu positioning * Add product counter * Add select all option * Update snapshots * Update messages * Remove unused import * Add chips * Add test tags * Update snapshots * Change number of max chips * Add accordion tags * Update messages
This commit is contained in:
parent
5062d35270
commit
df5aea6200
56 changed files with 2440 additions and 425 deletions
|
@ -19,6 +19,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
|
|
||||||
- Add weight field and fix warehouse country selection - #597 by @dominik-zeglen
|
- Add weight field and fix warehouse country selection - #597 by @dominik-zeglen
|
||||||
- Fix weight based rate update - #604 by @dominik-zeglen
|
- Fix weight based rate update - #604 by @dominik-zeglen
|
||||||
|
- Add product export - #620 by @dominik-zeglen
|
||||||
|
|
||||||
## 2.10.0
|
## 2.10.0
|
||||||
|
|
||||||
|
|
|
@ -1806,6 +1806,16 @@
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Confirm"
|
"string": "Confirm"
|
||||||
},
|
},
|
||||||
|
"src_dot_containers_dot_BackgroundTasks_dot_exportFailedTitle": {
|
||||||
|
"string": "Product Export Failed"
|
||||||
|
},
|
||||||
|
"src_dot_containers_dot_BackgroundTasks_dot_exportFinishedText": {
|
||||||
|
"string": "Product export has finished and was sent to your email address."
|
||||||
|
},
|
||||||
|
"src_dot_containers_dot_BackgroundTasks_dot_exportFinishedTitle": {
|
||||||
|
"context": "csv file exporting has finished, header",
|
||||||
|
"string": "Exporting CSV finished"
|
||||||
|
},
|
||||||
"src_dot_containers_dot_BackgroundTasks_dot_invoiceGenerateFinishedText": {
|
"src_dot_containers_dot_BackgroundTasks_dot_invoiceGenerateFinishedText": {
|
||||||
"string": "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
|
"string": "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
|
||||||
},
|
},
|
||||||
|
@ -2597,6 +2607,10 @@
|
||||||
"context": "navigation section name",
|
"context": "navigation section name",
|
||||||
"string": "Navigation"
|
"string": "Navigation"
|
||||||
},
|
},
|
||||||
|
"src_dot_nextStep": {
|
||||||
|
"context": "go to next step, button",
|
||||||
|
"string": "Next"
|
||||||
|
},
|
||||||
"src_dot_no": {
|
"src_dot_no": {
|
||||||
"string": "No"
|
"string": "No"
|
||||||
},
|
},
|
||||||
|
@ -3722,12 +3736,6 @@
|
||||||
"src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_902296540": {
|
"src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_902296540": {
|
||||||
"string": "Search Attributes"
|
"string": "Search Attributes"
|
||||||
},
|
},
|
||||||
"src_dot_productTypes_dot_components_dot_ProductTypeAttributeEditDialog_dot_1228425832": {
|
|
||||||
"string": "Attribute name"
|
|
||||||
},
|
|
||||||
"src_dot_productTypes_dot_components_dot_ProductTypeAttributeEditDialog_dot_335542212": {
|
|
||||||
"string": "Attribute values"
|
|
||||||
},
|
|
||||||
"src_dot_productTypes_dot_components_dot_ProductTypeAttributeUnassignDialog_dot_404238501": {
|
"src_dot_productTypes_dot_components_dot_ProductTypeAttributeUnassignDialog_dot_404238501": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Unassign Attribute From Product Type"
|
"string": "Unassign Attribute From Product Type"
|
||||||
|
@ -3915,6 +3923,145 @@
|
||||||
"context": "product name",
|
"context": "product name",
|
||||||
"string": "Name"
|
"string": "Name"
|
||||||
},
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_108342258": {
|
||||||
|
"context": "button",
|
||||||
|
"string": "Load More"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_1459686496": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Visibility"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_1547327218": {
|
||||||
|
"context": "there are more elements of list that are hidden",
|
||||||
|
"string": "and {number} more"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_1755013298": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Category"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_1890035856": {
|
||||||
|
"context": "informations about product organization, header",
|
||||||
|
"string": "Product Organization"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_1952810469": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Type"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2051669917": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Cost Price"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2119710854": {
|
||||||
|
"context": "informations about product seo, header",
|
||||||
|
"string": "SEO Information"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2167661409": {
|
||||||
|
"context": "export selected products to csv file",
|
||||||
|
"string": "Selected products ({number})"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_222873645": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Collections"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2318723509": {
|
||||||
|
"context": "export products to csv file, choice field label",
|
||||||
|
"string": "Export information for:"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2355065897": {
|
||||||
|
"context": "export all products to csv file",
|
||||||
|
"string": "All products ({number})"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2659464408": {
|
||||||
|
"context": "product export to csv file, header",
|
||||||
|
"string": "Information exported"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2693217446": {
|
||||||
|
"context": "export products as csv or spreadsheet file",
|
||||||
|
"string": "Export as:"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2883720012": {
|
||||||
|
"context": "export products to csv file, button",
|
||||||
|
"string": "export products"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_2949617129": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Product Images"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3012202273": {
|
||||||
|
"context": "export products to csv file, dialog header",
|
||||||
|
"string": "Export Information"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3365843236": {
|
||||||
|
"context": "product export to csv file, header",
|
||||||
|
"string": "Export Settings"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3374163063": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Description"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3441755345": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Charge Taxes"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3443345452": {
|
||||||
|
"context": "selectt all options",
|
||||||
|
"string": "Select All"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3518309850": {
|
||||||
|
"context": "export products as spreadsheet",
|
||||||
|
"string": "Spreadsheet for Excel, Numbers etc."
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3544554440": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Variant Weight"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3599582104": {
|
||||||
|
"string": "Search Atrtibuttes"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_3919525499": {
|
||||||
|
"context": "informations about product stock, header",
|
||||||
|
"string": "Inventory Information"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_4118932547": {
|
||||||
|
"context": "export products as csv file",
|
||||||
|
"string": "Plain CSV file"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_4160582036": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Variant Price"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_472026385": {
|
||||||
|
"context": "select product informations to be exported",
|
||||||
|
"string": "Information exported:"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_524117994": {
|
||||||
|
"context": "input helper text, search attributes",
|
||||||
|
"string": "Search by attribute name"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_636461959": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Name"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_693960049": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "SKU"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_700651641": {
|
||||||
|
"context": "export filtered products to csv file",
|
||||||
|
"string": "Current search ({number})"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_734825715": {
|
||||||
|
"context": "informations about product prices etc, header",
|
||||||
|
"string": "Financial Information"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_746695941": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Weight"
|
||||||
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductExportDialog_dot_897437458": {
|
||||||
|
"context": "product field",
|
||||||
|
"string": "Variant Images"
|
||||||
|
},
|
||||||
"src_dot_products_dot_components_dot_ProductImageNavigation_dot_3060635772": {
|
"src_dot_products_dot_components_dot_ProductImageNavigation_dot_3060635772": {
|
||||||
"context": "section header",
|
"context": "section header",
|
||||||
"string": "All Photos"
|
"string": "All Photos"
|
||||||
|
@ -3951,6 +4098,10 @@
|
||||||
"context": "product type",
|
"context": "product type",
|
||||||
"string": "Type"
|
"string": "Type"
|
||||||
},
|
},
|
||||||
|
"src_dot_products_dot_components_dot_ProductListPage_dot_2059406063": {
|
||||||
|
"context": "export products to csv file, button",
|
||||||
|
"string": "Export Products"
|
||||||
|
},
|
||||||
"src_dot_products_dot_components_dot_ProductListPage_dot_3550330425": {
|
"src_dot_products_dot_components_dot_ProductListPage_dot_3550330425": {
|
||||||
"string": "Search Products..."
|
"string": "Search Products..."
|
||||||
},
|
},
|
||||||
|
@ -4339,6 +4490,10 @@
|
||||||
"context": "dialog content",
|
"context": "dialog content",
|
||||||
"string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
"string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
||||||
},
|
},
|
||||||
|
"src_dot_products_dot_views_dot_ProductList_dot_1505423810": {
|
||||||
|
"context": "waiting for export to end, header",
|
||||||
|
"string": "Exporting CSV"
|
||||||
|
},
|
||||||
"src_dot_products_dot_views_dot_ProductList_dot_1547167026": {
|
"src_dot_products_dot_views_dot_ProductList_dot_1547167026": {
|
||||||
"context": "publish product, button",
|
"context": "publish product, button",
|
||||||
"string": "Publish"
|
"string": "Publish"
|
||||||
|
@ -4367,6 +4522,9 @@
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete Products"
|
"string": "Delete Products"
|
||||||
},
|
},
|
||||||
|
"src_dot_products_dot_views_dot_ProductList_dot_44832327": {
|
||||||
|
"string": "We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address"
|
||||||
|
},
|
||||||
"src_dot_products_dot_views_dot_ProductUpdate_dot_1177237881": {
|
"src_dot_products_dot_views_dot_ProductUpdate_dot_1177237881": {
|
||||||
"context": "dialog content",
|
"context": "dialog content",
|
||||||
"string": "{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}"
|
"string": "{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}"
|
||||||
|
@ -4430,6 +4588,10 @@
|
||||||
"src_dot_savedChanges": {
|
"src_dot_savedChanges": {
|
||||||
"string": "Saved changes"
|
"string": "Saved changes"
|
||||||
},
|
},
|
||||||
|
"src_dot_selectAll": {
|
||||||
|
"context": "select all options, button",
|
||||||
|
"string": "Select All"
|
||||||
|
},
|
||||||
"src_dot_send": {
|
"src_dot_send": {
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Send"
|
"string": "Send"
|
||||||
|
|
121
schema.graphql
121
schema.graphql
|
@ -2232,6 +2232,108 @@ type Error {
|
||||||
message: String
|
message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportError {
|
||||||
|
field: String
|
||||||
|
message: String
|
||||||
|
code: ExportErrorCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExportErrorCode {
|
||||||
|
INVALID
|
||||||
|
NOT_FOUND
|
||||||
|
REQUIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportEvent implements Node {
|
||||||
|
id: ID!
|
||||||
|
date: DateTime!
|
||||||
|
type: ExportEventsEnum!
|
||||||
|
user: User
|
||||||
|
app: App
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExportEventsEnum {
|
||||||
|
EXPORT_PENDING
|
||||||
|
EXPORT_SUCCESS
|
||||||
|
EXPORT_FAILED
|
||||||
|
EXPORT_DELETED
|
||||||
|
EXPORTED_FILE_SENT
|
||||||
|
EXPORT_FAILED_INFO_SENT
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportFile implements Node & Job {
|
||||||
|
id: ID!
|
||||||
|
user: User
|
||||||
|
app: App
|
||||||
|
status: JobStatusEnum!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
url: String
|
||||||
|
events: [ExportEvent!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportFileCountableConnection {
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
edges: [ExportFileCountableEdge!]!
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportFileCountableEdge {
|
||||||
|
node: ExportFile!
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportFileFilterInput {
|
||||||
|
createdAt: DateTimeRangeInput
|
||||||
|
updatedAt: DateTimeRangeInput
|
||||||
|
status: JobStatusEnum
|
||||||
|
user: String
|
||||||
|
app: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExportFileSortField {
|
||||||
|
STATUS
|
||||||
|
CREATED_AT
|
||||||
|
UPDATED_AT
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportFileSortingInput {
|
||||||
|
direction: OrderDirection!
|
||||||
|
field: ExportFileSortField!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportInfoInput {
|
||||||
|
attributes: [ID!]
|
||||||
|
warehouses: [ID!]
|
||||||
|
fields: [ProductFieldEnum!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportProducts {
|
||||||
|
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||||
|
exportFile: ExportFile
|
||||||
|
exportErrors: [ExportError!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportProductsInput {
|
||||||
|
scope: ExportScope!
|
||||||
|
filter: ProductFilterInput
|
||||||
|
ids: [ID!]
|
||||||
|
exportInfo: ExportInfoInput
|
||||||
|
fileType: FileTypesEnum!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExportScope {
|
||||||
|
ALL
|
||||||
|
IDS
|
||||||
|
FILTER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FileTypesEnum {
|
||||||
|
CSV
|
||||||
|
XLSX
|
||||||
|
}
|
||||||
|
|
||||||
type Fulfillment implements Node & ObjectWithMetadata {
|
type Fulfillment implements Node & ObjectWithMetadata {
|
||||||
id: ID!
|
id: ID!
|
||||||
fulfillmentOrder: Int!
|
fulfillmentOrder: Int!
|
||||||
|
@ -4657,6 +4759,23 @@ enum ProductErrorCode {
|
||||||
VARIANT_NO_DIGITAL_CONTENT
|
VARIANT_NO_DIGITAL_CONTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ProductFieldEnum {
|
||||||
|
NAME
|
||||||
|
DESCRIPTION
|
||||||
|
PRODUCT_TYPE
|
||||||
|
CATEGORY
|
||||||
|
VISIBLE
|
||||||
|
PRODUCT_WEIGHT
|
||||||
|
COLLECTIONS
|
||||||
|
CHARGE_TAXES
|
||||||
|
PRODUCT_IMAGES
|
||||||
|
VARIANT_SKU
|
||||||
|
VARIANT_PRICE
|
||||||
|
COST_PRICE
|
||||||
|
VARIANT_WEIGHT
|
||||||
|
VARIANT_IMAGES
|
||||||
|
}
|
||||||
|
|
||||||
input ProductFilterInput {
|
input ProductFilterInput {
|
||||||
isPublished: Boolean
|
isPublished: Boolean
|
||||||
collections: [ID]
|
collections: [ID]
|
||||||
|
@ -5446,6 +5565,8 @@ type Query {
|
||||||
first: Int
|
first: Int
|
||||||
last: Int
|
last: Int
|
||||||
): VoucherCountableConnection
|
): VoucherCountableConnection
|
||||||
|
exportFile(id: ID!): ExportFile
|
||||||
|
exportFiles(filter: ExportFileFilterInput, sortBy: ExportFileSortingInput, before: String, after: String, first: Int, last: Int): ExportFileCountableConnection
|
||||||
taxTypes: [TaxType]
|
taxTypes: [TaxType]
|
||||||
checkout(token: UUID): Checkout
|
checkout(token: UUID): Checkout
|
||||||
checkouts(
|
checkouts(
|
||||||
|
|
21
src/components/Accordion/Accordion.stories.tsx
Normal file
21
src/components/Accordion/Accordion.stories.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Accordion from "./Accordion";
|
||||||
|
|
||||||
|
storiesOf("Generics / Accordion", module)
|
||||||
|
.addDecorator(Decorator)
|
||||||
|
.addDecorator(CardDecorator)
|
||||||
|
.add("default", () => <Accordion title="Title">Content</Accordion>)
|
||||||
|
.add("opened", () => (
|
||||||
|
<Accordion title="Title" initialExpand={true}>
|
||||||
|
Content
|
||||||
|
</Accordion>
|
||||||
|
))
|
||||||
|
.add("with quick peek", () => (
|
||||||
|
<Accordion title="Title" quickPeek="Quick Peek">
|
||||||
|
Content
|
||||||
|
</Accordion>
|
||||||
|
));
|
80
src/components/Accordion/Accordion.tsx
Normal file
80
src/components/Accordion/Accordion.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import AddIcon from "@material-ui/icons/Add";
|
||||||
|
import RemoveIcon from "@material-ui/icons/Remove";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Hr from "../Hr";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
content: {
|
||||||
|
padding: theme.spacing(3, 0)
|
||||||
|
},
|
||||||
|
expandButton: {
|
||||||
|
position: "relative",
|
||||||
|
right: theme.spacing(-2),
|
||||||
|
top: theme.spacing(0.5)
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: theme.spacing(0, 3)
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
},
|
||||||
|
titleText: {
|
||||||
|
padding: theme.spacing(2, 0)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "Accordion"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface AccordionProps {
|
||||||
|
className?: string;
|
||||||
|
initialExpand?: boolean;
|
||||||
|
quickPeek?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Accordion: React.FC<AccordionProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
initialExpand,
|
||||||
|
quickPeek,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
const [expanded, setExpanded] = React.useState(!!initialExpand);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(classes.root, className)} {...props}>
|
||||||
|
<div className={classes.title}>
|
||||||
|
<Typography className={classes.titleText}>{title}</Typography>
|
||||||
|
<div className={classes.expandButton}>
|
||||||
|
<IconButton onClick={() => setExpanded(!expanded)}>
|
||||||
|
{expanded ? <RemoveIcon /> : <AddIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(expanded || !!quickPeek) && (
|
||||||
|
<>
|
||||||
|
<Hr />
|
||||||
|
<div className={classes.content}>
|
||||||
|
{quickPeek ? (expanded ? children : quickPeek) : children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Accordion.displayName = "Accordion";
|
||||||
|
export default Accordion;
|
2
src/components/Accordion/index.ts
Normal file
2
src/components/Accordion/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./Accordion";
|
||||||
|
export * from "./Accordion";
|
|
@ -1,6 +1,10 @@
|
||||||
|
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||||
|
import Grow from "@material-ui/core/Grow";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import Menu from "@material-ui/core/Menu";
|
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from "@material-ui/core/MenuItem";
|
||||||
|
import MenuList from "@material-ui/core/MenuList";
|
||||||
|
import Paper from "@material-ui/core/Paper";
|
||||||
|
import Popper from "@material-ui/core/Popper";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -9,6 +13,7 @@ const ITEM_HEIGHT = 48;
|
||||||
|
|
||||||
export interface CardMenuItem {
|
export interface CardMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
testId?: string;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,34 +31,58 @@ const useStyles = makeStyles(
|
||||||
height: 32,
|
height: 32,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
width: 32
|
width: 32
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ name: "CardMenu" }
|
{ name: "CardMenu" }
|
||||||
);
|
);
|
||||||
|
|
||||||
const CardMenu: React.FC<CardMenuProps> = props => {
|
const CardMenu: React.FC<CardMenuProps> = props => {
|
||||||
const { className, disabled, menuItems } = props;
|
const { className, disabled, menuItems, ...rest } = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
const [anchorEl, setAnchor] = React.useState<HTMLElement | null>(null);
|
const anchorRef = React.useRef<HTMLButtonElement | null>(null);
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<any>) => {
|
const handleToggle = () => setOpen(prevOpen => !prevOpen);
|
||||||
setAnchor(event.currentTarget);
|
|
||||||
|
const handleClose = (event: React.MouseEvent<EventTarget>) => {
|
||||||
|
if (
|
||||||
|
anchorRef.current &&
|
||||||
|
anchorRef.current.contains(event.target as HTMLElement)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleListKeyDown = (event: React.KeyboardEvent) => {
|
||||||
setAnchor(null);
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuClick = (menuItemIndex: number) => {
|
const prevOpen = React.useRef(open);
|
||||||
menuItems[menuItemIndex].onSelect();
|
React.useEffect(() => {
|
||||||
handleClose();
|
if (prevOpen.current === true && open === false) {
|
||||||
};
|
anchorRef.current!.focus();
|
||||||
|
}
|
||||||
|
|
||||||
const open = !!anchorEl;
|
prevOpen.current = open;
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleMenuClick = (index: number) => {
|
||||||
|
menuItems[index].onSelect();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className} {...rest}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="More"
|
aria-label="More"
|
||||||
aria-owns={open ? "long-menu" : null}
|
aria-owns={open ? "long-menu" : null}
|
||||||
|
@ -61,31 +90,47 @@ const CardMenu: React.FC<CardMenuProps> = props => {
|
||||||
className={classes.iconButton}
|
className={classes.iconButton}
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={handleClick}
|
ref={anchorRef}
|
||||||
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
<MoreVertIcon />
|
<MoreVertIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Menu
|
<Popper
|
||||||
id="long-menu"
|
placement="bottom-end"
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
anchorEl={anchorRef.current}
|
||||||
PaperProps={{
|
transition
|
||||||
style: {
|
>
|
||||||
maxHeight: ITEM_HEIGHT * 4.5
|
{({ TransitionProps, placement }) => (
|
||||||
// width: 200
|
<Grow
|
||||||
}
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === "bottom" ? "right top" : "right bottom"
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList
|
||||||
|
autoFocusItem={open}
|
||||||
|
id="menu-list-grow"
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
>
|
>
|
||||||
{menuItems.map((menuItem, menuItemIndex) => (
|
{menuItems.map((menuItem, menuItemIndex) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => handleMenuClick(menuItemIndex)}
|
onClick={() => handleMenuClick(menuItemIndex)}
|
||||||
key={menuItem.label}
|
key={menuItem.label}
|
||||||
|
data-test={menuItem.testId}
|
||||||
>
|
>
|
||||||
{menuItem.label}
|
{menuItem.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,8 +23,8 @@ const useStyles = makeStyles(
|
||||||
color: theme.palette.common.white
|
color: theme.palette.common.white
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
background: fade(theme.palette.secondary.main, 0.8),
|
background: fade(theme.palette.primary.main, 0.8),
|
||||||
borderRadius: 8,
|
borderRadius: 18,
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
padding: "6px 12px"
|
padding: "6px 12px"
|
||||||
|
|
84
src/components/CreatorSteps/CreatorSteps.tsx
Normal file
84
src/components/CreatorSteps/CreatorSteps.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface Step<T> {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: theme.spacing(3)
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
paddingBottom: theme.spacing(),
|
||||||
|
userSelect: "none"
|
||||||
|
},
|
||||||
|
tabActive: {
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
tabVisited: {
|
||||||
|
borderBottom: `3px solid ${theme.palette.primary.main}`,
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "CreatorSteps"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface CreatorStepsProps<T> {
|
||||||
|
currentStep: T;
|
||||||
|
steps: Array<Step<T>>;
|
||||||
|
onStepClick: (step: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCreatorSteps<T extends string | number>() {
|
||||||
|
const CreatorSteps: React.FC<CreatorStepsProps<T>> = ({
|
||||||
|
currentStep,
|
||||||
|
steps,
|
||||||
|
onStepClick
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
{steps.map((step, stepIndex) => {
|
||||||
|
const visitedStep =
|
||||||
|
steps.findIndex(step => step.value === currentStep) >= stepIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(classes.tab, {
|
||||||
|
[classes.tabActive]: step.value === currentStep,
|
||||||
|
[classes.tabVisited]: visitedStep
|
||||||
|
})}
|
||||||
|
onClick={visitedStep ? () => onStepClick(step.value) : undefined}
|
||||||
|
key={step.value}
|
||||||
|
>
|
||||||
|
<Typography className={classes.label} variant="caption">
|
||||||
|
{step.label}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
CreatorSteps.displayName = "CreatorSteps";
|
||||||
|
|
||||||
|
return CreatorSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeCreatorSteps;
|
2
src/components/CreatorSteps/index.ts
Normal file
2
src/components/CreatorSteps/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./CreatorSteps";
|
||||||
|
export { default } from "./CreatorSteps";
|
|
@ -36,8 +36,11 @@ const useStyles = makeStyles(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface RadioGroupFieldChoice {
|
export interface RadioGroupFieldChoice<
|
||||||
value: string;
|
T extends string | number = string | number
|
||||||
|
> {
|
||||||
|
disabled?: boolean;
|
||||||
|
value: T;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ interface RadioGroupFieldProps {
|
||||||
hint?: string;
|
hint?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
value: string;
|
value: string | number;
|
||||||
onChange: (event: React.ChangeEvent<any>) => void;
|
onChange: (event: React.ChangeEvent<any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +90,7 @@ export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
|
||||||
{choices.length > 0 ? (
|
{choices.length > 0 ? (
|
||||||
choices.map(choice => (
|
choices.map(choice => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
|
disabled={choice.disabled}
|
||||||
value={choice.value}
|
value={choice.value}
|
||||||
className={classes.radioLabel}
|
className={classes.radioLabel}
|
||||||
control={<Radio color="primary" />}
|
control={<Radio color="primary" />}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export interface IMessage {
|
||||||
autohide?: number;
|
autohide?: number;
|
||||||
expandText?: string;
|
expandText?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
text: string;
|
text: React.ReactNode;
|
||||||
onUndo?: () => void;
|
onUndo?: () => void;
|
||||||
status?: Status;
|
status?: Status;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
|
import { JobStatusEnum } from "@saleor/types/globalTypes";
|
||||||
import { renderHook } from "@testing-library/react-hooks";
|
import { renderHook } from "@testing-library/react-hooks";
|
||||||
import { createMockClient } from "mock-apollo-client";
|
import { createMockClient, RequestHandlerResponse } from "mock-apollo-client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
backgroundTasksRefreshTime,
|
backgroundTasksRefreshTime,
|
||||||
useBackgroundTasks
|
useBackgroundTasks
|
||||||
} from "./BackgroundTasksProvider";
|
} from "./BackgroundTasksProvider";
|
||||||
import { OnCompletedTaskData, Task, TaskData, TaskStatus } from "./types";
|
import { checkExportFileStatus } from "./queries";
|
||||||
|
import { Task, TaskData, TaskStatus } from "./types";
|
||||||
|
import { CheckExportFileStatus } from "./types/CheckExportFileStatus";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
function renderBackgroundTasks() {
|
function renderBackgroundTasks() {
|
||||||
const mockClient = createMockClient();
|
const mockClient = createMockClient();
|
||||||
|
mockClient.setRequestHandler(checkExportFileStatus, () =>
|
||||||
|
Promise.resolve<RequestHandlerResponse<CheckExportFileStatus>>({
|
||||||
|
data: {
|
||||||
|
exportFile: {
|
||||||
|
__typename: "ExportFile",
|
||||||
|
id: "123",
|
||||||
|
status: JobStatusEnum.SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
const intl = {
|
const intl = {
|
||||||
formatMessage: ({ defaultMessage }) => defaultMessage
|
formatMessage: ({ defaultMessage }) => defaultMessage
|
||||||
};
|
};
|
||||||
|
@ -80,33 +94,6 @@ describe("Background task provider", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can handle task failure", done => {
|
|
||||||
const handle = jest.fn<Promise<TaskStatus>, []>(
|
|
||||||
() => new Promise(resolve => resolve(TaskStatus.FAILURE))
|
|
||||||
);
|
|
||||||
const onCompleted = jest.fn((data: OnCompletedTaskData) =>
|
|
||||||
expect(data.status).toBe(TaskStatus.FAILURE)
|
|
||||||
);
|
|
||||||
const onError = jest.fn();
|
|
||||||
|
|
||||||
const { result } = renderBackgroundTasks();
|
|
||||||
|
|
||||||
result.current.queue(Task.CUSTOM, {
|
|
||||||
handle,
|
|
||||||
onCompleted,
|
|
||||||
onError
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(handle).toHaveBeenCalledTimes(1);
|
|
||||||
expect(onCompleted).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can cancel task", done => {
|
it("can cancel task", done => {
|
||||||
const onCompleted = jest.fn();
|
const onCompleted = jest.fn();
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { IMessageContext } from "@saleor/components/messages";
|
import { IMessageContext } from "@saleor/components/messages";
|
||||||
import useNotifier from "@saleor/hooks/useNotifier";
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
import { checkOrderInvoicesStatus } from "@saleor/orders/queries";
|
|
||||||
import ApolloClient from "apollo-client";
|
import ApolloClient from "apollo-client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useApolloClient } from "react-apollo";
|
import { useApolloClient } from "react-apollo";
|
||||||
import { IntlShape, useIntl } from "react-intl";
|
import { IntlShape, useIntl } from "react-intl";
|
||||||
|
|
||||||
import BackgroundTasksContext from "./context";
|
import BackgroundTasksContext from "./context";
|
||||||
import { handleTask, queueCustom, queueInvoiceGenerate } from "./tasks";
|
import { checkExportFileStatus, checkOrderInvoicesStatus } from "./queries";
|
||||||
|
import {
|
||||||
|
handleTask,
|
||||||
|
queueCustom,
|
||||||
|
queueExport,
|
||||||
|
queueInvoiceGenerate
|
||||||
|
} from "./tasks";
|
||||||
import { QueuedTask, Task, TaskData, TaskStatus } from "./types";
|
import { QueuedTask, Task, TaskData, TaskStatus } from "./types";
|
||||||
|
|
||||||
export const backgroundTasksRefreshTime = 15 * 1000;
|
export const backgroundTasksRefreshTime = 15 * 1000;
|
||||||
|
@ -81,6 +86,22 @@ export function useBackgroundTasks(
|
||||||
intl
|
intl
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case Task.EXPORT:
|
||||||
|
queueExport(
|
||||||
|
idCounter.current,
|
||||||
|
tasks,
|
||||||
|
() =>
|
||||||
|
apolloClient.query({
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
query: checkExportFileStatus,
|
||||||
|
variables: {
|
||||||
|
id: data.id
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
notify,
|
||||||
|
intl
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return idCounter.current;
|
return idCounter.current;
|
||||||
|
|
27
src/containers/BackgroundTasks/messages.ts
Normal file
27
src/containers/BackgroundTasks/messages.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export default defineMessages({
|
||||||
|
exportFailedTitle: {
|
||||||
|
defaultMessage: "Product Export Failed"
|
||||||
|
},
|
||||||
|
exportFinishedText: {
|
||||||
|
defaultMessage:
|
||||||
|
"Product export has finished and was sent to your email address."
|
||||||
|
},
|
||||||
|
exportFinishedTitle: {
|
||||||
|
defaultMessage: "Exporting CSV finished",
|
||||||
|
description: "csv file exporting has finished, header"
|
||||||
|
},
|
||||||
|
invoiceGenerateFinishedText: {
|
||||||
|
defaultMessage:
|
||||||
|
"Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
|
||||||
|
},
|
||||||
|
invoiceGenerateFinishedTitle: {
|
||||||
|
defaultMessage: "Invoice Generated",
|
||||||
|
description: "invoice generating has finished, header"
|
||||||
|
},
|
||||||
|
invoiceGenerationFailedTitle: {
|
||||||
|
defaultMessage: "Invoice Generation",
|
||||||
|
description: "dialog header, title"
|
||||||
|
}
|
||||||
|
});
|
23
src/containers/BackgroundTasks/queries.ts
Normal file
23
src/containers/BackgroundTasks/queries.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { invoiceFragment } from "@saleor/fragments/orders";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
export const checkExportFileStatus = gql`
|
||||||
|
query CheckExportFileStatus($id: ID!) {
|
||||||
|
exportFile(id: $id) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const checkOrderInvoicesStatus = gql`
|
||||||
|
${invoiceFragment}
|
||||||
|
query CheckOrderInvoicesStatus($id: ID!) {
|
||||||
|
order(id: $id) {
|
||||||
|
id
|
||||||
|
invoices {
|
||||||
|
...InvoiceFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
|
@ -1,31 +1,29 @@
|
||||||
import { IMessageContext } from "@saleor/components/messages";
|
import { IMessageContext } from "@saleor/components/messages";
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
import { CheckOrderInvoicesStatus } from "@saleor/orders/types/CheckOrderInvoicesStatus";
|
|
||||||
import { JobStatusEnum } from "@saleor/types/globalTypes";
|
import { JobStatusEnum } from "@saleor/types/globalTypes";
|
||||||
import { ApolloQueryResult } from "apollo-client";
|
import { ApolloQueryResult } from "apollo-client";
|
||||||
import { defineMessages, IntlShape } from "react-intl";
|
import { IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
import {
|
import {
|
||||||
InvoiceGenerateParams,
|
InvoiceGenerateParams,
|
||||||
QueuedTask,
|
QueuedTask,
|
||||||
TaskData,
|
TaskData,
|
||||||
TaskStatus
|
TaskStatus
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { CheckExportFileStatus } from "./types/CheckExportFileStatus";
|
||||||
|
import { CheckOrderInvoicesStatus } from "./types/CheckOrderInvoicesStatus";
|
||||||
|
|
||||||
export const messages = defineMessages({
|
function getTaskStatus(jobStatus: JobStatusEnum): TaskStatus {
|
||||||
invoiceGenerateFinishedText: {
|
switch (jobStatus) {
|
||||||
defaultMessage:
|
case JobStatusEnum.SUCCESS:
|
||||||
"Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
|
return TaskStatus.SUCCESS;
|
||||||
},
|
case JobStatusEnum.PENDING:
|
||||||
invoiceGenerateFinishedTitle: {
|
return TaskStatus.PENDING;
|
||||||
defaultMessage: "Invoice Generated",
|
default:
|
||||||
description: "invoice generating has finished, header"
|
return TaskStatus.FAILURE;
|
||||||
},
|
}
|
||||||
invoiceGenerationFailedTitle: {
|
|
||||||
defaultMessage: "Invoice Generation",
|
|
||||||
description: "dialog header, title"
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
export async function handleTask(task: QueuedTask): Promise<TaskStatus> {
|
export async function handleTask(task: QueuedTask): Promise<TaskStatus> {
|
||||||
let status = TaskStatus.PENDING;
|
let status = TaskStatus.PENDING;
|
||||||
|
@ -89,11 +87,7 @@ export function queueInvoiceGenerate(
|
||||||
invoice => invoice.id === generateInvoice.invoiceId
|
invoice => invoice.id === generateInvoice.invoiceId
|
||||||
).status;
|
).status;
|
||||||
|
|
||||||
return status === JobStatusEnum.SUCCESS
|
return getTaskStatus(status);
|
||||||
? TaskStatus.SUCCESS
|
|
||||||
: status === JobStatusEnum.PENDING
|
|
||||||
? TaskStatus.PENDING
|
|
||||||
: TaskStatus.FAILURE;
|
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
onCompleted: data =>
|
onCompleted: data =>
|
||||||
|
@ -104,6 +98,7 @@ export function queueInvoiceGenerate(
|
||||||
title: intl.formatMessage(messages.invoiceGenerateFinishedTitle)
|
title: intl.formatMessage(messages.invoiceGenerateFinishedTitle)
|
||||||
})
|
})
|
||||||
: notify({
|
: notify({
|
||||||
|
status: "error",
|
||||||
text: intl.formatMessage(commonMessages.somethingWentWrong),
|
text: intl.formatMessage(commonMessages.somethingWentWrong),
|
||||||
title: intl.formatMessage(messages.invoiceGenerationFailedTitle)
|
title: intl.formatMessage(messages.invoiceGenerationFailedTitle)
|
||||||
}),
|
}),
|
||||||
|
@ -112,3 +107,38 @@ export function queueInvoiceGenerate(
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queueExport(
|
||||||
|
id: number,
|
||||||
|
tasks: React.MutableRefObject<QueuedTask[]>,
|
||||||
|
fetch: () => Promise<ApolloQueryResult<CheckExportFileStatus>>,
|
||||||
|
notify: IMessageContext,
|
||||||
|
intl: IntlShape
|
||||||
|
) {
|
||||||
|
tasks.current = [
|
||||||
|
...tasks.current,
|
||||||
|
{
|
||||||
|
handle: async () => {
|
||||||
|
const result = await fetch();
|
||||||
|
const status = result.data.exportFile.status;
|
||||||
|
|
||||||
|
return getTaskStatus(status);
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
onCompleted: data =>
|
||||||
|
data.status === TaskStatus.SUCCESS
|
||||||
|
? notify({
|
||||||
|
status: "success",
|
||||||
|
text: intl.formatMessage(messages.exportFinishedText),
|
||||||
|
title: intl.formatMessage(messages.exportFinishedTitle)
|
||||||
|
})
|
||||||
|
: notify({
|
||||||
|
status: "error",
|
||||||
|
text: intl.formatMessage(commonMessages.somethingWentWrong),
|
||||||
|
title: intl.formatMessage(messages.exportFailedTitle)
|
||||||
|
}),
|
||||||
|
onError: handleError,
|
||||||
|
status: TaskStatus.PENDING
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export enum Task {
|
export enum Task {
|
||||||
CUSTOM,
|
CUSTOM,
|
||||||
|
EXPORT,
|
||||||
INVOICE_GENERATE
|
INVOICE_GENERATE
|
||||||
}
|
}
|
||||||
export enum TaskStatus {
|
export enum TaskStatus {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { JobStatusEnum } from "./../../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: CheckExportFileStatus
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface CheckExportFileStatus_exportFile {
|
||||||
|
__typename: "ExportFile";
|
||||||
|
id: string;
|
||||||
|
status: JobStatusEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckExportFileStatus {
|
||||||
|
exportFile: CheckExportFileStatus_exportFile | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckExportFileStatusVariables {
|
||||||
|
id: string;
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { JobStatusEnum } from "./../../types/globalTypes";
|
import { JobStatusEnum } from "./../../../types/globalTypes";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: CheckOrderInvoicesStatus
|
// GraphQL query operation: CheckOrderInvoicesStatus
|
|
@ -120,3 +120,10 @@ export const appErrorFragment = gql`
|
||||||
permissions
|
permissions
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const exportErrorFragment = gql`
|
||||||
|
fragment ExportErrorFragment on ExportError {
|
||||||
|
code
|
||||||
|
field
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -241,3 +241,11 @@ export const fragmentVariant = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const exportFileFragment = gql`
|
||||||
|
fragment ExportFileFragment on ExportFile {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
url
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
15
src/fragments/types/ExportErrorFragment.ts
Normal file
15
src/fragments/types/ExportErrorFragment.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { ExportErrorCode } from "./../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL fragment: ExportErrorFragment
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ExportErrorFragment {
|
||||||
|
__typename: "ExportError";
|
||||||
|
code: ExportErrorCode;
|
||||||
|
field: string | null;
|
||||||
|
}
|
16
src/fragments/types/ExportFileFragment.ts
Normal file
16
src/fragments/types/ExportFileFragment.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { JobStatusEnum } from "./../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL fragment: ExportFileFragment
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ExportFileFragment {
|
||||||
|
__typename: "ExportFile";
|
||||||
|
id: string;
|
||||||
|
status: JobStatusEnum;
|
||||||
|
url: string | null;
|
||||||
|
}
|
|
@ -132,6 +132,10 @@ export const buttonMessages = defineMessages({
|
||||||
defaultMessage: "Manage",
|
defaultMessage: "Manage",
|
||||||
description: "button"
|
description: "button"
|
||||||
},
|
},
|
||||||
|
nextStep: {
|
||||||
|
defaultMessage: "Next",
|
||||||
|
description: "go to next step, button"
|
||||||
|
},
|
||||||
ok: {
|
ok: {
|
||||||
defaultMessage: "OK",
|
defaultMessage: "OK",
|
||||||
description: "button"
|
description: "button"
|
||||||
|
@ -144,6 +148,10 @@ export const buttonMessages = defineMessages({
|
||||||
defaultMessage: "Save",
|
defaultMessage: "Save",
|
||||||
description: "button"
|
description: "button"
|
||||||
},
|
},
|
||||||
|
selectAll: {
|
||||||
|
defaultMessage: "Select All",
|
||||||
|
description: "select all options, button"
|
||||||
|
},
|
||||||
send: {
|
send: {
|
||||||
defaultMessage: "Send",
|
defaultMessage: "Send",
|
||||||
description: "button"
|
description: "button"
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import { fragmentAddress } from "@saleor/fragments/address";
|
import { fragmentAddress } from "@saleor/fragments/address";
|
||||||
import {
|
import { fragmentOrderDetails } from "@saleor/fragments/orders";
|
||||||
fragmentOrderDetails,
|
|
||||||
invoiceFragment
|
|
||||||
} from "@saleor/fragments/orders";
|
|
||||||
import makeQuery from "@saleor/hooks/makeQuery";
|
import makeQuery from "@saleor/hooks/makeQuery";
|
||||||
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
|
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
@ -231,15 +228,3 @@ export const useOrderFulfillData = makeQuery<
|
||||||
OrderFulfillData,
|
OrderFulfillData,
|
||||||
OrderFulfillDataVariables
|
OrderFulfillDataVariables
|
||||||
>(orderFulfillData);
|
>(orderFulfillData);
|
||||||
|
|
||||||
export const checkOrderInvoicesStatus = gql`
|
|
||||||
${invoiceFragment}
|
|
||||||
query CheckOrderInvoicesStatus($id: ID!) {
|
|
||||||
order(id: $id) {
|
|
||||||
id
|
|
||||||
invoices {
|
|
||||||
...InvoiceFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { messages } from "@saleor/containers/BackgroundTasks/tasks";
|
import messages from "@saleor/containers/BackgroundTasks/messages";
|
||||||
import useNavigator from "@saleor/hooks/useNavigator";
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
import useNotifier from "@saleor/hooks/useNotifier";
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
|
|
@ -30,7 +30,7 @@ import React from "react";
|
||||||
import InfiniteScroll from "react-infinite-scroller";
|
import InfiniteScroll from "react-infinite-scroller";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { SearchAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAttributes";
|
import { SearchAvailableAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
|
@ -63,7 +63,7 @@ export interface AssignAttributeDialogProps extends FetchMoreProps {
|
||||||
confirmButtonState: ConfirmButtonTransitionState;
|
confirmButtonState: ConfirmButtonTransitionState;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
attributes: SearchAttributes_productType_availableAttributes_edges_node[];
|
attributes: SearchAvailableAttributes_productType_availableAttributes_edges_node[];
|
||||||
selected: string[];
|
selected: string[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onFetch: (query: string) => void;
|
onFetch: (query: string) => void;
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
|
||||||
import DialogActions from "@material-ui/core/DialogActions";
|
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
|
||||||
import TextField from "@material-ui/core/TextField";
|
|
||||||
import Form from "@saleor/components/Form";
|
|
||||||
import { FormSpacer } from "@saleor/components/FormSpacer";
|
|
||||||
import ListField from "@saleor/components/ListField";
|
|
||||||
import { buttonMessages } from "@saleor/intl";
|
|
||||||
import { UserError } from "@saleor/types";
|
|
||||||
import { getFieldError } from "@saleor/utils/errors";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
|
|
||||||
export interface FormData {
|
|
||||||
name: string;
|
|
||||||
values: Array<{
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductTypeAttributeEditDialogProps {
|
|
||||||
disabled: boolean;
|
|
||||||
errors: UserError[];
|
|
||||||
name: string;
|
|
||||||
opened: boolean;
|
|
||||||
title: string;
|
|
||||||
values: Array<{
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}>;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: (data: FormData) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductTypeAttributeEditDialog: React.FC<ProductTypeAttributeEditDialogProps> = ({
|
|
||||||
disabled,
|
|
||||||
errors,
|
|
||||||
name,
|
|
||||||
opened,
|
|
||||||
title,
|
|
||||||
values,
|
|
||||||
onClose,
|
|
||||||
onConfirm
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const initialForm: FormData = {
|
|
||||||
name: name || "",
|
|
||||||
values: values || []
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog onClose={onClose} open={opened}>
|
|
||||||
<Form initial={initialForm} onSubmit={onConfirm}>
|
|
||||||
{({ change, data }) => (
|
|
||||||
<>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
error={!!getFieldError(errors, "name")}
|
|
||||||
fullWidth
|
|
||||||
label={intl.formatMessage({
|
|
||||||
defaultMessage: "Attribute name"
|
|
||||||
})}
|
|
||||||
helperText={getFieldError(errors, "name")?.message}
|
|
||||||
name="name"
|
|
||||||
value={data.name}
|
|
||||||
onChange={change}
|
|
||||||
/>
|
|
||||||
<FormSpacer />
|
|
||||||
<ListField
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={disabled}
|
|
||||||
error={
|
|
||||||
!!getFieldError(errors, "values") ||
|
|
||||||
!!getFieldError(errors, "addValues") ||
|
|
||||||
!!getFieldError(errors, "removeValues")
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
name="values"
|
|
||||||
label={intl.formatMessage({
|
|
||||||
defaultMessage: "Attribute values"
|
|
||||||
})}
|
|
||||||
helperText={
|
|
||||||
getFieldError(errors, "values") ||
|
|
||||||
getFieldError(errors, "addValues") ||
|
|
||||||
getFieldError(errors, "removeValues")
|
|
||||||
}
|
|
||||||
values={data.values}
|
|
||||||
onChange={change}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose}>
|
|
||||||
<FormattedMessage {...buttonMessages.back} />
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" variant="contained" type="submit">
|
|
||||||
<FormattedMessage {...buttonMessages.confirm} />
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ProductTypeAttributeEditDialog.displayName = "ProductTypeAttributeEditDialog";
|
|
||||||
export default ProductTypeAttributeEditDialog;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./ProductTypeAttributeEditDialog";
|
|
||||||
export * from "./ProductTypeAttributeEditDialog";
|
|
|
@ -3,13 +3,13 @@ import makeSearch from "@saleor/hooks/makeSearch";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SearchAttributes,
|
SearchAvailableAttributes,
|
||||||
SearchAttributesVariables
|
SearchAvailableAttributesVariables
|
||||||
} from "./types/SearchAttributes";
|
} from "./types/SearchAvailableAttributes";
|
||||||
|
|
||||||
export const searchAttributes = gql`
|
export const searchAttributes = gql`
|
||||||
${pageInfoFragment}
|
${pageInfoFragment}
|
||||||
query SearchAttributes(
|
query SearchAvailableAttributes(
|
||||||
$id: ID!
|
$id: ID!
|
||||||
$after: String
|
$after: String
|
||||||
$first: Int!
|
$first: Int!
|
||||||
|
@ -37,9 +37,10 @@ export const searchAttributes = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default makeSearch<SearchAttributes, SearchAttributesVariables>(
|
export default makeSearch<
|
||||||
searchAttributes,
|
SearchAvailableAttributes,
|
||||||
result =>
|
SearchAvailableAttributesVariables
|
||||||
|
>(searchAttributes, result =>
|
||||||
result.loadMore(
|
result.loadMore(
|
||||||
(prev, next) => {
|
(prev, next) => {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: SearchAttributes
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface SearchAttributes_productType_availableAttributes_edges_node {
|
|
||||||
__typename: "Attribute";
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
slug: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributes_productType_availableAttributes_edges {
|
|
||||||
__typename: "AttributeCountableEdge";
|
|
||||||
node: SearchAttributes_productType_availableAttributes_edges_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributes_productType_availableAttributes_pageInfo {
|
|
||||||
__typename: "PageInfo";
|
|
||||||
endCursor: string | null;
|
|
||||||
hasNextPage: boolean;
|
|
||||||
hasPreviousPage: boolean;
|
|
||||||
startCursor: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributes_productType_availableAttributes {
|
|
||||||
__typename: "AttributeCountableConnection";
|
|
||||||
edges: SearchAttributes_productType_availableAttributes_edges[];
|
|
||||||
pageInfo: SearchAttributes_productType_availableAttributes_pageInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributes_productType {
|
|
||||||
__typename: "ProductType";
|
|
||||||
id: string;
|
|
||||||
availableAttributes: SearchAttributes_productType_availableAttributes | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributes {
|
|
||||||
productType: SearchAttributes_productType | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchAttributesVariables {
|
|
||||||
id: string;
|
|
||||||
after?: string | null;
|
|
||||||
first: number;
|
|
||||||
query: string;
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: SearchAvailableAttributes
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes_productType_availableAttributes_edges_node {
|
||||||
|
__typename: "Attribute";
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
slug: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes_productType_availableAttributes_edges {
|
||||||
|
__typename: "AttributeCountableEdge";
|
||||||
|
node: SearchAvailableAttributes_productType_availableAttributes_edges_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes_productType_availableAttributes_pageInfo {
|
||||||
|
__typename: "PageInfo";
|
||||||
|
endCursor: string | null;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
startCursor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes_productType_availableAttributes {
|
||||||
|
__typename: "AttributeCountableConnection";
|
||||||
|
edges: SearchAvailableAttributes_productType_availableAttributes_edges[];
|
||||||
|
pageInfo: SearchAvailableAttributes_productType_availableAttributes_pageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes_productType {
|
||||||
|
__typename: "ProductType";
|
||||||
|
id: string;
|
||||||
|
availableAttributes: SearchAvailableAttributes_productType_availableAttributes | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributes {
|
||||||
|
productType: SearchAvailableAttributes_productType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAvailableAttributesVariables {
|
||||||
|
id: string;
|
||||||
|
after?: string | null;
|
||||||
|
first: number;
|
||||||
|
query: string;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import Decorator from "@saleor/storybook/Decorator";
|
||||||
|
import {
|
||||||
|
ExportErrorCode,
|
||||||
|
ExportProductsInput
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { attributes } from "../../../attributes/fixtures";
|
||||||
|
import ProductExportDialog, {
|
||||||
|
ProductExportDialogProps
|
||||||
|
} from "./ProductExportDialog";
|
||||||
|
|
||||||
|
const props: ProductExportDialogProps = {
|
||||||
|
attributes: attributes.map(attr => ({
|
||||||
|
__typename: "Attribute",
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name
|
||||||
|
})),
|
||||||
|
confirmButtonState: "default",
|
||||||
|
errors: [],
|
||||||
|
hasMore: true,
|
||||||
|
loading: true,
|
||||||
|
onClose: () => undefined,
|
||||||
|
onFetch: () => undefined,
|
||||||
|
onFetchMore: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
open: true,
|
||||||
|
productQuantity: {
|
||||||
|
all: 100,
|
||||||
|
filter: 32
|
||||||
|
},
|
||||||
|
selectedProducts: 18
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Views / Products / Export / Export settings", module)
|
||||||
|
.addDecorator(Decorator)
|
||||||
|
.add("interactive", () => <ProductExportDialog {...props} />)
|
||||||
|
.add("no products selected", () => (
|
||||||
|
<ProductExportDialog {...props} selectedProducts={0} />
|
||||||
|
))
|
||||||
|
.add("errors", () => (
|
||||||
|
<ProductExportDialog
|
||||||
|
{...props}
|
||||||
|
errors={(["fileType", "scope", null] as Array<
|
||||||
|
keyof ExportProductsInput | null
|
||||||
|
>).map(field => ({
|
||||||
|
__typename: "ExportError",
|
||||||
|
code: ExportErrorCode.INVALID,
|
||||||
|
field
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
));
|
|
@ -0,0 +1,227 @@
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Dialog from "@material-ui/core/Dialog";
|
||||||
|
import DialogActions from "@material-ui/core/DialogActions";
|
||||||
|
import DialogContent from "@material-ui/core/DialogContent";
|
||||||
|
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import ConfirmButton, {
|
||||||
|
ConfirmButtonTransitionState
|
||||||
|
} from "@saleor/components/ConfirmButton";
|
||||||
|
import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
|
||||||
|
import Form from "@saleor/components/Form";
|
||||||
|
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||||
|
import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
|
||||||
|
import { FormChange } from "@saleor/hooks/useForm";
|
||||||
|
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
|
||||||
|
import useWizard from "@saleor/hooks/useWizard";
|
||||||
|
import { buttonMessages } from "@saleor/intl";
|
||||||
|
import { SearchAttributes_search_edges_node } from "@saleor/searches/types/SearchAttributes";
|
||||||
|
import { DialogProps, FetchMoreProps } from "@saleor/types";
|
||||||
|
import {
|
||||||
|
ExportProductsInput,
|
||||||
|
ExportScope,
|
||||||
|
FileTypesEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import getExportErrorMessage from "@saleor/utils/errors/export";
|
||||||
|
import { toggle } from "@saleor/utils/lists";
|
||||||
|
import { mapNodeToChoice } from "@saleor/utils/maps";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import ProductExportDialogInfo, {
|
||||||
|
attributeNamePrefix
|
||||||
|
} from "./ProductExportDialogInfo";
|
||||||
|
import ProductExportDialogSettings, {
|
||||||
|
ProductQuantity
|
||||||
|
} from "./ProductExportDialogSettings";
|
||||||
|
|
||||||
|
export enum ProductExportStep {
|
||||||
|
INFO,
|
||||||
|
SETTINGS
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSteps(): Array<Step<ProductExportStep>> {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Information exported",
|
||||||
|
description: "product export to csv file, header"
|
||||||
|
}),
|
||||||
|
value: ProductExportStep.INFO
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Export Settings",
|
||||||
|
description: "product export to csv file, header"
|
||||||
|
}),
|
||||||
|
value: ProductExportStep.SETTINGS
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialForm: ExportProductsInput = {
|
||||||
|
exportInfo: {
|
||||||
|
attributes: [],
|
||||||
|
fields: []
|
||||||
|
},
|
||||||
|
fileType: FileTypesEnum.CSV,
|
||||||
|
scope: ExportScope.ALL
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProductExportSteps = makeCreatorSteps<ProductExportStep>();
|
||||||
|
|
||||||
|
export interface ProductExportDialogProps extends DialogProps, FetchMoreProps {
|
||||||
|
attributes: SearchAttributes_search_edges_node[];
|
||||||
|
confirmButtonState: ConfirmButtonTransitionState;
|
||||||
|
errors: ExportErrorFragment[];
|
||||||
|
productQuantity: ProductQuantity;
|
||||||
|
selectedProducts: number;
|
||||||
|
onFetch: (query: string) => void;
|
||||||
|
onSubmit: (data: ExportProductsInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
||||||
|
attributes,
|
||||||
|
confirmButtonState,
|
||||||
|
errors,
|
||||||
|
productQuantity,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
open,
|
||||||
|
selectedProducts,
|
||||||
|
...fetchMoreProps
|
||||||
|
}) => {
|
||||||
|
const [step, { next, prev, set: setStep }] = useWizard(
|
||||||
|
ProductExportStep.INFO,
|
||||||
|
[ProductExportStep.INFO, ProductExportStep.SETTINGS]
|
||||||
|
);
|
||||||
|
const steps = useSteps();
|
||||||
|
const dialogErrors = useModalDialogErrors(errors, open);
|
||||||
|
const notFormErrors = dialogErrors.filter(err => !err.field);
|
||||||
|
const intl = useIntl();
|
||||||
|
const [selectedAttributes, setSelectedAttributes] = React.useState<
|
||||||
|
MultiAutocompleteChoiceType[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const attributeChoices = mapNodeToChoice(attributes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onClose={onClose} open={open} maxWidth="sm" fullWidth>
|
||||||
|
<Form initial={initialForm} onSubmit={onSubmit}>
|
||||||
|
{({ change, data }) => {
|
||||||
|
const handleAttributeSelect: FormChange = event => {
|
||||||
|
const id = event.target.name.substr(attributeNamePrefix.length);
|
||||||
|
|
||||||
|
change({
|
||||||
|
target: {
|
||||||
|
name: "exportInfo",
|
||||||
|
value: {
|
||||||
|
...data.exportInfo,
|
||||||
|
attributes: toggle(
|
||||||
|
id,
|
||||||
|
data.exportInfo.attributes,
|
||||||
|
(a, b) => a === b
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const choice = attributeChoices.find(choice => choice.value === id);
|
||||||
|
|
||||||
|
setSelectedAttributes(
|
||||||
|
toggle(choice, selectedAttributes, (a, b) => a.value === b.value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogTitle>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Export Information"
|
||||||
|
description="export products to csv file, dialog header"
|
||||||
|
/>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<ProductExportSteps
|
||||||
|
currentStep={step}
|
||||||
|
steps={steps}
|
||||||
|
onStepClick={setStep}
|
||||||
|
/>
|
||||||
|
{step === ProductExportStep.INFO && (
|
||||||
|
<ProductExportDialogInfo
|
||||||
|
attributes={attributeChoices}
|
||||||
|
data={data}
|
||||||
|
selectedAttributes={selectedAttributes}
|
||||||
|
onAttrtibuteSelect={handleAttributeSelect}
|
||||||
|
onChange={change}
|
||||||
|
{...fetchMoreProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === ProductExportStep.SETTINGS && (
|
||||||
|
<ProductExportDialogSettings
|
||||||
|
data={data}
|
||||||
|
errors={dialogErrors}
|
||||||
|
productQuantity={productQuantity}
|
||||||
|
selectedProducts={selectedProducts}
|
||||||
|
onChange={change}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
{notFormErrors.length > 0 && (
|
||||||
|
<DialogContent>
|
||||||
|
{notFormErrors.map(err => (
|
||||||
|
<Typography color="error" key={err.field + err.code}>
|
||||||
|
{getExportErrorMessage(err, intl)}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</DialogContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
{step === ProductExportStep.INFO && (
|
||||||
|
<Button onClick={onClose} data-test="cancel">
|
||||||
|
<FormattedMessage {...buttonMessages.cancel} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === ProductExportStep.SETTINGS && (
|
||||||
|
<Button onClick={prev} data-test="back">
|
||||||
|
<FormattedMessage {...buttonMessages.back} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === ProductExportStep.INFO && (
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={next}
|
||||||
|
data-test="next"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...buttonMessages.nextStep} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === ProductExportStep.SETTINGS && (
|
||||||
|
<ConfirmButton
|
||||||
|
transitionState={confirmButtonState}
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
data-test="submit"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="export products"
|
||||||
|
description="export products to csv file, button"
|
||||||
|
/>
|
||||||
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductExportDialog.displayName = "ProductExportDialog";
|
||||||
|
export default ProductExportDialog;
|
|
@ -0,0 +1,448 @@
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Checkbox from "@material-ui/core/Checkbox";
|
||||||
|
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import Accordion, { AccordionProps } from "@saleor/components/Accordion";
|
||||||
|
import Chip from "@saleor/components/Chip";
|
||||||
|
import Hr from "@saleor/components/Hr";
|
||||||
|
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||||
|
import { ChangeEvent } from "@saleor/hooks/useForm";
|
||||||
|
import useSearchQuery from "@saleor/hooks/useSearchQuery";
|
||||||
|
import { sectionNames } from "@saleor/intl";
|
||||||
|
import { FetchMoreProps } from "@saleor/types";
|
||||||
|
import {
|
||||||
|
ExportProductsInput,
|
||||||
|
ProductFieldEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { toggle } from "@saleor/utils/lists";
|
||||||
|
import React from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
export const attributeNamePrefix = "attribute-";
|
||||||
|
const maxChips = 4;
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
accordion: {
|
||||||
|
marginBottom: theme.spacing(2)
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
position: "relative",
|
||||||
|
right: -theme.spacing(1.5)
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
marginRight: theme.spacing()
|
||||||
|
},
|
||||||
|
dialogLabel: {
|
||||||
|
marginBottom: theme.spacing(2)
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
marginTop: theme.spacing(3)
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
"&&": {
|
||||||
|
overflow: "visible"
|
||||||
|
},
|
||||||
|
justifyContent: "space-between",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
loadMoreContainer: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginTop: theme.spacing(2)
|
||||||
|
},
|
||||||
|
moreLabel: {
|
||||||
|
display: "inline-block",
|
||||||
|
marginBottom: theme.spacing()
|
||||||
|
},
|
||||||
|
quickPeekContainer: {
|
||||||
|
marginBottom: theme.spacing(-1)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "ProductExportDialogInfo"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Option: React.FC<{
|
||||||
|
checked: boolean;
|
||||||
|
name: string;
|
||||||
|
onChange: (event: ChangeEvent) => void;
|
||||||
|
}> = ({ checked, children, name, onChange }) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
color="primary"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
className={classes.checkbox}
|
||||||
|
checked={checked}
|
||||||
|
color="primary"
|
||||||
|
name={name}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={classes.label}
|
||||||
|
label={children}
|
||||||
|
labelPlacement="start"
|
||||||
|
></FormControlLabel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FieldAccordion: React.FC<AccordionProps & {
|
||||||
|
data: ExportProductsInput;
|
||||||
|
fields: ProductFieldEnum[];
|
||||||
|
onChange: (event: ChangeEvent) => void;
|
||||||
|
onToggleAll: (field: ProductFieldEnum[], setTo: boolean) => void;
|
||||||
|
}> = ({ data, fields, onChange, onToggleAll, ...props }) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const fieldNames: Record<ProductFieldEnum, string> = {
|
||||||
|
[ProductFieldEnum.CATEGORY]: intl.formatMessage({
|
||||||
|
defaultMessage: "Category",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.CHARGE_TAXES]: intl.formatMessage({
|
||||||
|
defaultMessage: "Charge Taxes",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.COLLECTIONS]: intl.formatMessage({
|
||||||
|
defaultMessage: "Collections",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.COST_PRICE]: intl.formatMessage({
|
||||||
|
defaultMessage: "Cost Price",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.DESCRIPTION]: intl.formatMessage({
|
||||||
|
defaultMessage: "Description",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.NAME]: intl.formatMessage({
|
||||||
|
defaultMessage: "Name",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.PRODUCT_IMAGES]: intl.formatMessage({
|
||||||
|
defaultMessage: "Product Images",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.PRODUCT_TYPE]: intl.formatMessage({
|
||||||
|
defaultMessage: "Type",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.PRODUCT_WEIGHT]: intl.formatMessage({
|
||||||
|
defaultMessage: "Weight",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.VARIANT_IMAGES]: intl.formatMessage({
|
||||||
|
defaultMessage: "Variant Images",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.VARIANT_PRICE]: intl.formatMessage({
|
||||||
|
defaultMessage: "Variant Price",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.VARIANT_SKU]: intl.formatMessage({
|
||||||
|
defaultMessage: "SKU",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.VARIANT_WEIGHT]: intl.formatMessage({
|
||||||
|
defaultMessage: "Variant Weight",
|
||||||
|
description: "product field"
|
||||||
|
}),
|
||||||
|
[ProductFieldEnum.VISIBLE]: intl.formatMessage({
|
||||||
|
defaultMessage: "Visibility",
|
||||||
|
description: "product field"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedAll = fields.every(field =>
|
||||||
|
data.exportInfo.fields.includes(field)
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFields = data.exportInfo.fields.filter(field =>
|
||||||
|
fields.includes(field)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
quickPeek={
|
||||||
|
selectedFields.length > 0 && (
|
||||||
|
<div className={classes.quickPeekContainer}>
|
||||||
|
{selectedFields.slice(0, maxChips).map(field => (
|
||||||
|
<Chip
|
||||||
|
className={classes.chip}
|
||||||
|
label={fieldNames[field]}
|
||||||
|
onClose={() =>
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: field,
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{selectedFields.length > maxChips && (
|
||||||
|
<Typography className={classes.moreLabel} variant="caption">
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="and {number} more"
|
||||||
|
description="there are more elements of list that are hidden"
|
||||||
|
values={{
|
||||||
|
number: selectedFields.length - maxChips
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Option
|
||||||
|
checked={selectedAll}
|
||||||
|
name="all"
|
||||||
|
onChange={() => onToggleAll(fields, !selectedAll)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Select All"
|
||||||
|
description="selectt all options"
|
||||||
|
/>
|
||||||
|
</Option>
|
||||||
|
{fields.map(field => (
|
||||||
|
<Option
|
||||||
|
checked={data.exportInfo.fields.includes(field)}
|
||||||
|
name={field}
|
||||||
|
onChange={onChange}
|
||||||
|
key={field}
|
||||||
|
>
|
||||||
|
{fieldNames[field]}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProductExportDialogInfoProps extends FetchMoreProps {
|
||||||
|
attributes: MultiAutocompleteChoiceType[];
|
||||||
|
data: ExportProductsInput;
|
||||||
|
selectedAttributes: MultiAutocompleteChoiceType[];
|
||||||
|
onAttrtibuteSelect: (event: ChangeEvent) => void;
|
||||||
|
onChange: (event: ChangeEvent) => void;
|
||||||
|
onFetch: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
||||||
|
attributes,
|
||||||
|
data,
|
||||||
|
hasMore,
|
||||||
|
selectedAttributes,
|
||||||
|
loading,
|
||||||
|
onAttrtibuteSelect,
|
||||||
|
onChange,
|
||||||
|
onFetch,
|
||||||
|
onFetchMore
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
const intl = useIntl();
|
||||||
|
const [query, onQueryChange] = useSearchQuery(onFetch);
|
||||||
|
|
||||||
|
const handleFieldChange = (event: ChangeEvent) =>
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: "exportInfo",
|
||||||
|
value: {
|
||||||
|
...data.exportInfo,
|
||||||
|
fields: toggle(
|
||||||
|
event.target.name,
|
||||||
|
data.exportInfo.fields,
|
||||||
|
(a, b) => a === b
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleAllFields = (fields: ProductFieldEnum[], setTo: boolean) =>
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: "exportInfo",
|
||||||
|
value: {
|
||||||
|
...data.exportInfo,
|
||||||
|
fields: setTo
|
||||||
|
? [
|
||||||
|
...data.exportInfo.fields,
|
||||||
|
...fields.filter(
|
||||||
|
field => !data.exportInfo.fields.includes(field)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: data.exportInfo.fields.filter(field => !fields.includes(field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography className={classes.dialogLabel}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Information exported:"
|
||||||
|
description="select product informations to be exported"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<FieldAccordion
|
||||||
|
className={classes.accordion}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Product Organization",
|
||||||
|
description: "informations about product organization, header"
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
fields={[
|
||||||
|
ProductFieldEnum.CATEGORY,
|
||||||
|
ProductFieldEnum.COLLECTIONS,
|
||||||
|
ProductFieldEnum.PRODUCT_TYPE
|
||||||
|
]}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
onToggleAll={handleToggleAllFields}
|
||||||
|
data-test="organization"
|
||||||
|
/>
|
||||||
|
<Accordion
|
||||||
|
className={classes.accordion}
|
||||||
|
title={intl.formatMessage(sectionNames.attributes)}
|
||||||
|
quickPeek={
|
||||||
|
selectedAttributes.length > 0 && (
|
||||||
|
<div className={classes.quickPeekContainer}>
|
||||||
|
{selectedAttributes.slice(0, maxChips).map(attribute => (
|
||||||
|
<Chip
|
||||||
|
className={classes.chip}
|
||||||
|
label={attribute.label}
|
||||||
|
onClose={() =>
|
||||||
|
onAttrtibuteSelect({
|
||||||
|
target: {
|
||||||
|
name: attributeNamePrefix + attribute.value,
|
||||||
|
value: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{selectedAttributes.length > maxChips && (
|
||||||
|
<Typography className={classes.moreLabel} variant="caption">
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="and {number} more"
|
||||||
|
description="there are more elements of list that are hidden"
|
||||||
|
values={{
|
||||||
|
number: selectedAttributes.length - maxChips
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data-test="attributes"
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
name="query"
|
||||||
|
value={query}
|
||||||
|
onChange={onQueryChange}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
defaultMessage: "Search Atrtibuttes"
|
||||||
|
})}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
defaultMessage: "Search by attribute name",
|
||||||
|
description: "input helper text, search attributes"
|
||||||
|
})}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
autoComplete: "off",
|
||||||
|
endAdornment: loading && <CircularProgress size={16} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Hr className={classes.hr} />
|
||||||
|
{attributes.map(attribute => (
|
||||||
|
<Option
|
||||||
|
checked={data.exportInfo.attributes.includes(attribute.value)}
|
||||||
|
name={attributeNamePrefix + attribute.value}
|
||||||
|
onChange={onAttrtibuteSelect}
|
||||||
|
key={attribute.value}
|
||||||
|
>
|
||||||
|
{attribute.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
{(hasMore || loading) && (
|
||||||
|
<div className={classes.loadMoreContainer}>
|
||||||
|
{hasMore && !loading && (
|
||||||
|
<Button color="primary" onClick={onFetchMore}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Load More"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{loading && <CircularProgress size={32} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
<FieldAccordion
|
||||||
|
className={classes.accordion}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Financial Information",
|
||||||
|
description: "informations about product prices etc, header"
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
fields={[
|
||||||
|
ProductFieldEnum.CHARGE_TAXES,
|
||||||
|
ProductFieldEnum.COST_PRICE,
|
||||||
|
ProductFieldEnum.VARIANT_PRICE,
|
||||||
|
ProductFieldEnum.VISIBLE
|
||||||
|
]}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
onToggleAll={handleToggleAllFields}
|
||||||
|
data-test="financial"
|
||||||
|
/>
|
||||||
|
<FieldAccordion
|
||||||
|
className={classes.accordion}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Inventory Information",
|
||||||
|
description: "informations about product stock, header"
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
fields={[
|
||||||
|
ProductFieldEnum.PRODUCT_WEIGHT,
|
||||||
|
ProductFieldEnum.VARIANT_SKU,
|
||||||
|
ProductFieldEnum.VARIANT_WEIGHT
|
||||||
|
]}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
onToggleAll={handleToggleAllFields}
|
||||||
|
data-test="inventory"
|
||||||
|
/>
|
||||||
|
<FieldAccordion
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "SEO Information",
|
||||||
|
description: "informations about product seo, header"
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
fields={[
|
||||||
|
ProductFieldEnum.DESCRIPTION,
|
||||||
|
ProductFieldEnum.NAME,
|
||||||
|
ProductFieldEnum.PRODUCT_IMAGES,
|
||||||
|
ProductFieldEnum.VARIANT_IMAGES
|
||||||
|
]}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
onToggleAll={handleToggleAllFields}
|
||||||
|
data-test="seo"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductExportDialogInfo.displayName = "ProductExportDialogInfo";
|
||||||
|
export default ProductExportDialogInfo;
|
|
@ -0,0 +1,144 @@
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import Hr from "@saleor/components/Hr";
|
||||||
|
import RadioGroupField, {
|
||||||
|
RadioGroupFieldChoice
|
||||||
|
} from "@saleor/components/RadioGroupField";
|
||||||
|
import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
|
||||||
|
import { ChangeEvent } from "@saleor/hooks/useForm";
|
||||||
|
import {
|
||||||
|
ExportProductsInput,
|
||||||
|
ExportScope,
|
||||||
|
FileTypesEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { getFormErrors } from "@saleor/utils/errors";
|
||||||
|
import getExportErrorMessage from "@saleor/utils/errors/export";
|
||||||
|
import React from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
hr: {
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
marginTop: theme.spacing(3)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "ProductExportDialogSettings"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ProductQuantity = Record<"all" | "filter", number>;
|
||||||
|
export interface ProductExportDialogSettingsProps {
|
||||||
|
data: ExportProductsInput;
|
||||||
|
errors: ExportErrorFragment[];
|
||||||
|
productQuantity: ProductQuantity;
|
||||||
|
selectedProducts: number;
|
||||||
|
onChange: (event: ChangeEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFields: Array<keyof ExportProductsInput> = ["fileType", "scope"];
|
||||||
|
|
||||||
|
const ProductExportDialogSettings: React.FC<ProductExportDialogSettingsProps> = ({
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
onChange,
|
||||||
|
productQuantity,
|
||||||
|
selectedProducts
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const formErrors = getFormErrors(formFields, errors);
|
||||||
|
|
||||||
|
const productsToExportChoices: Array<RadioGroupFieldChoice<ExportScope>> = [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "All products ({number})",
|
||||||
|
description: "export all products to csv file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: productQuantity.all || "..."
|
||||||
|
}
|
||||||
|
),
|
||||||
|
value: ExportScope.ALL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
disabled: selectedProducts === 0,
|
||||||
|
label: intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Selected products ({number})",
|
||||||
|
description: "export selected products to csv file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: selectedProducts
|
||||||
|
}
|
||||||
|
),
|
||||||
|
value: ExportScope.IDS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Current search ({number})",
|
||||||
|
description: "export filtered products to csv file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: productQuantity.filter || "..."
|
||||||
|
}
|
||||||
|
),
|
||||||
|
value: ExportScope.FILTER
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const productExportTypeChoices: Array<RadioGroupFieldChoice<
|
||||||
|
FileTypesEnum
|
||||||
|
>> = [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Spreadsheet for Excel, Numbers etc.",
|
||||||
|
description: "export products as spreadsheet"
|
||||||
|
}),
|
||||||
|
value: FileTypesEnum.XLSX
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Plain CSV file",
|
||||||
|
description: "export products as csv file"
|
||||||
|
}),
|
||||||
|
value: FileTypesEnum.CSV
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RadioGroupField
|
||||||
|
choices={productsToExportChoices}
|
||||||
|
error={!!formErrors.scope}
|
||||||
|
hint={getExportErrorMessage(formErrors.scope, intl)}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
defaultMessage: "Export information for:",
|
||||||
|
description: "export products to csv file, choice field label"
|
||||||
|
})}
|
||||||
|
name={"scope" as keyof ExportProductsInput}
|
||||||
|
onChange={onChange}
|
||||||
|
value={data.scope}
|
||||||
|
/>
|
||||||
|
<Hr className={classes.hr} />
|
||||||
|
<RadioGroupField
|
||||||
|
choices={productExportTypeChoices}
|
||||||
|
error={!!formErrors.fileType}
|
||||||
|
hint={getExportErrorMessage(formErrors.fileType, intl)}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
defaultMessage: "Export as:",
|
||||||
|
description: "export products as csv or spreadsheet file"
|
||||||
|
})}
|
||||||
|
name={"fileType" as keyof ExportProductsInput}
|
||||||
|
onChange={onChange}
|
||||||
|
value={data.fileType}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductExportDialogSettings.displayName = "ProductExportDialogSettings";
|
||||||
|
export default ProductExportDialogSettings;
|
2
src/products/components/ProductExportDialog/index.ts
Normal file
2
src/products/components/ProductExportDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./ProductExportDialog";
|
||||||
|
export { default } from "./ProductExportDialog";
|
|
@ -1,6 +1,7 @@
|
||||||
import Button from "@material-ui/core/Button";
|
import Button from "@material-ui/core/Button";
|
||||||
import Card from "@material-ui/core/Card";
|
import Card from "@material-ui/core/Card";
|
||||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import CardMenu from "@saleor/components/CardMenu";
|
||||||
import ColumnPicker, {
|
import ColumnPicker, {
|
||||||
ColumnPickerChoice
|
ColumnPickerChoice
|
||||||
} from "@saleor/components/ColumnPicker";
|
} from "@saleor/components/ColumnPicker";
|
||||||
|
@ -44,12 +45,13 @@ export interface ProductListPageProps
|
||||||
gridAttributes: GridAttributes_grid_edges_node[];
|
gridAttributes: GridAttributes_grid_edges_node[];
|
||||||
totalGridAttributes: number;
|
totalGridAttributes: number;
|
||||||
products: ProductList_products_edges_node[];
|
products: ProductList_products_edges_node[];
|
||||||
|
onExport: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
columnPicker: {
|
columnPicker: {
|
||||||
marginRight: theme.spacing(3)
|
margin: theme.spacing(0, 3)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ name: "ProductListPage" }
|
{ name: "ProductListPage" }
|
||||||
|
@ -71,6 +73,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
totalGridAttributes,
|
totalGridAttributes,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onExport,
|
||||||
onFetchMore,
|
onFetchMore,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
|
@ -119,6 +122,19 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.products)}>
|
<PageHeader title={intl.formatMessage(sectionNames.products)}>
|
||||||
|
<CardMenu
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Export Products",
|
||||||
|
description: "export products to csv file, button"
|
||||||
|
}),
|
||||||
|
onSelect: onExport,
|
||||||
|
testId: "export"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
data-test="menu"
|
||||||
|
/>
|
||||||
<ColumnPicker
|
<ColumnPicker
|
||||||
className={classes.columnPicker}
|
className={classes.columnPicker}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IntlShape, useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { ProductVariantCreatorStep } from "./types";
|
import { ProductVariantCreatorStep } from "./types";
|
||||||
|
|
||||||
interface Step {
|
function useSteps(): Array<Step<ProductVariantCreatorStep>> {
|
||||||
label: string;
|
const intl = useIntl();
|
||||||
value: ProductVariantCreatorStep;
|
|
||||||
}
|
|
||||||
function getSteps(intl: IntlShape): Step[] {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
|
@ -36,69 +32,27 @@ function getSteps(intl: IntlShape): Step[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const ProductVariantCreatorSteps = makeCreatorSteps<
|
||||||
theme => ({
|
ProductVariantCreatorStep
|
||||||
label: {
|
>();
|
||||||
fontSize: 14,
|
|
||||||
textAlign: "center"
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: theme.spacing(3)
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
flex: 1,
|
|
||||||
paddingBottom: theme.spacing(),
|
|
||||||
userSelect: "none"
|
|
||||||
},
|
|
||||||
tabActive: {
|
|
||||||
fontWeight: 600
|
|
||||||
},
|
|
||||||
tabVisited: {
|
|
||||||
borderBottom: `3px solid ${theme.palette.primary.main}`,
|
|
||||||
cursor: "pointer"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "ProductVariantCreatorTabs"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface ProductVariantCreatorTabsProps {
|
export interface ProductVariantCreatorTabsProps {
|
||||||
step: ProductVariantCreatorStep;
|
step: ProductVariantCreatorStep;
|
||||||
onStepClick: (step: ProductVariantCreatorStep) => void;
|
onStepClick: (step: ProductVariantCreatorStep) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = props => {
|
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = ({
|
||||||
const { step: currentStep, onStepClick } = props;
|
step: currentStep,
|
||||||
const classes = useStyles(props);
|
onStepClick
|
||||||
const intl = useIntl();
|
}) => {
|
||||||
const steps = getSteps(intl);
|
const steps = useSteps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<ProductVariantCreatorSteps
|
||||||
{steps.map((step, stepIndex) => {
|
currentStep={currentStep}
|
||||||
const visitedStep =
|
steps={steps}
|
||||||
steps.findIndex(step => step.value === currentStep) >= stepIndex;
|
onStepClick={onStepClick}
|
||||||
|
/>
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(classes.tab, {
|
|
||||||
[classes.tabActive]: step.value === currentStep,
|
|
||||||
[classes.tabVisited]: visitedStep
|
|
||||||
})}
|
|
||||||
onClick={visitedStep ? () => onStepClick(step.value) : undefined}
|
|
||||||
key={step.value}
|
|
||||||
>
|
|
||||||
<Typography className={classes.label} variant="caption">
|
|
||||||
{step.label}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import {
|
import {
|
||||||
bulkProductErrorFragment,
|
bulkProductErrorFragment,
|
||||||
bulkStockErrorFragment,
|
bulkStockErrorFragment,
|
||||||
|
exportErrorFragment,
|
||||||
productErrorFragment,
|
productErrorFragment,
|
||||||
stockErrorFragment
|
stockErrorFragment
|
||||||
} from "@saleor/fragments/errors";
|
} from "@saleor/fragments/errors";
|
||||||
import {
|
import {
|
||||||
|
exportFileFragment,
|
||||||
fragmentVariant,
|
fragmentVariant,
|
||||||
productFragmentDetails
|
productFragmentDetails
|
||||||
} from "@saleor/fragments/products";
|
} from "@saleor/fragments/products";
|
||||||
|
@ -22,6 +24,7 @@ import {
|
||||||
} from "./types/productBulkPublish";
|
} from "./types/productBulkPublish";
|
||||||
import { ProductCreate, ProductCreateVariables } from "./types/ProductCreate";
|
import { ProductCreate, ProductCreateVariables } from "./types/ProductCreate";
|
||||||
import { ProductDelete, ProductDeleteVariables } from "./types/ProductDelete";
|
import { ProductDelete, ProductDeleteVariables } from "./types/ProductDelete";
|
||||||
|
import { ProductExport, ProductExportVariables } from "./types/ProductExport";
|
||||||
import {
|
import {
|
||||||
ProductImageCreate,
|
ProductImageCreate,
|
||||||
ProductImageCreateVariables
|
ProductImageCreateVariables
|
||||||
|
@ -567,3 +570,22 @@ export const TypedProductVariantBulkDeleteMutation = TypedMutation<
|
||||||
ProductVariantBulkDelete,
|
ProductVariantBulkDelete,
|
||||||
ProductVariantBulkDeleteVariables
|
ProductVariantBulkDeleteVariables
|
||||||
>(ProductVariantBulkDeleteMutation);
|
>(ProductVariantBulkDeleteMutation);
|
||||||
|
|
||||||
|
export const productExportMutation = gql`
|
||||||
|
${exportFileFragment}
|
||||||
|
${exportErrorFragment}
|
||||||
|
mutation ProductExport($input: ExportProductsInput!) {
|
||||||
|
exportProducts(input: $input) {
|
||||||
|
exportFile {
|
||||||
|
...ExportFileFragment
|
||||||
|
}
|
||||||
|
errors: exportErrors {
|
||||||
|
...ExportErrorFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const useProductExport = makeMutation<
|
||||||
|
ProductExport,
|
||||||
|
ProductExportVariables
|
||||||
|
>(productExportMutation);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import makeQuery from "@saleor/hooks/makeQuery";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
import { TypedQuery } from "../queries";
|
import { TypedQuery } from "../queries";
|
||||||
|
import { CountAllProducts } from "./types/CountAllProducts";
|
||||||
import {
|
import {
|
||||||
CreateMultipleVariantsData,
|
CreateMultipleVariantsData,
|
||||||
CreateMultipleVariantsDataVariables
|
CreateMultipleVariantsDataVariables
|
||||||
|
@ -145,6 +146,7 @@ const productListQuery = gql`
|
||||||
startCursor
|
startCursor
|
||||||
endCursor
|
endCursor
|
||||||
}
|
}
|
||||||
|
totalCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -153,6 +155,17 @@ export const TypedProductListQuery = TypedQuery<
|
||||||
ProductListVariables
|
ProductListVariables
|
||||||
>(productListQuery);
|
>(productListQuery);
|
||||||
|
|
||||||
|
const countAllProductsQuery = gql`
|
||||||
|
query CountAllProducts {
|
||||||
|
products {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const useCountAllProducts = makeQuery<CountAllProducts, null>(
|
||||||
|
countAllProductsQuery
|
||||||
|
);
|
||||||
|
|
||||||
const productDetailsQuery = gql`
|
const productDetailsQuery = gql`
|
||||||
${productFragmentDetails}
|
${productFragmentDetails}
|
||||||
query ProductDetails($id: ID!) {
|
query ProductDetails($id: ID!) {
|
||||||
|
|
16
src/products/types/CountAllProducts.ts
Normal file
16
src/products/types/CountAllProducts.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: CountAllProducts
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface CountAllProducts_products {
|
||||||
|
__typename: "ProductCountableConnection";
|
||||||
|
totalCount: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountAllProducts {
|
||||||
|
products: CountAllProducts_products | null;
|
||||||
|
}
|
36
src/products/types/ProductExport.ts
Normal file
36
src/products/types/ProductExport.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { ExportProductsInput, JobStatusEnum, ExportErrorCode } from "./../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: ProductExport
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ProductExport_exportProducts_exportFile {
|
||||||
|
__typename: "ExportFile";
|
||||||
|
id: string;
|
||||||
|
status: JobStatusEnum;
|
||||||
|
url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductExport_exportProducts_errors {
|
||||||
|
__typename: "ExportError";
|
||||||
|
code: ExportErrorCode;
|
||||||
|
field: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductExport_exportProducts {
|
||||||
|
__typename: "ExportProducts";
|
||||||
|
exportFile: ProductExport_exportProducts_exportFile | null;
|
||||||
|
errors: ProductExport_exportProducts_errors[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductExport {
|
||||||
|
exportProducts: ProductExport_exportProducts | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductExportVariables {
|
||||||
|
input: ExportProductsInput;
|
||||||
|
}
|
|
@ -99,6 +99,7 @@ export interface ProductList_products {
|
||||||
__typename: "ProductCountableConnection";
|
__typename: "ProductCountableConnection";
|
||||||
edges: ProductList_products_edges[];
|
edges: ProductList_products_edges[];
|
||||||
pageInfo: ProductList_products_pageInfo;
|
pageInfo: ProductList_products_pageInfo;
|
||||||
|
totalCount: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductList {
|
export interface ProductList {
|
||||||
|
|
|
@ -23,6 +23,7 @@ export type ProductListUrlDialog =
|
||||||
| "publish"
|
| "publish"
|
||||||
| "unpublish"
|
| "unpublish"
|
||||||
| "delete"
|
| "delete"
|
||||||
|
| "export"
|
||||||
| TabActionDialog;
|
| TabActionDialog;
|
||||||
export enum ProductListUrlFiltersEnum {
|
export enum ProductListUrlFiltersEnum {
|
||||||
priceFrom = "priceFrom",
|
priceFrom = "priceFrom",
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
defaultListSettings,
|
defaultListSettings,
|
||||||
ProductListColumns
|
ProductListColumns
|
||||||
} from "@saleor/config";
|
} from "@saleor/config";
|
||||||
|
import { Task } from "@saleor/containers/BackgroundTasks/types";
|
||||||
|
import useBackgroundTask from "@saleor/hooks/useBackgroundTask";
|
||||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||||
import useListSettings from "@saleor/hooks/useListSettings";
|
import useListSettings from "@saleor/hooks/useListSettings";
|
||||||
import useNavigator from "@saleor/hooks/useNavigator";
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
|
@ -23,11 +25,13 @@ import usePaginator, {
|
||||||
import useShop from "@saleor/hooks/useShop";
|
import useShop from "@saleor/hooks/useShop";
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
import { maybe } from "@saleor/misc";
|
import { maybe } from "@saleor/misc";
|
||||||
|
import ProductExportDialog from "@saleor/products/components/ProductExportDialog";
|
||||||
import {
|
import {
|
||||||
getAttributeIdFromColumnValue,
|
getAttributeIdFromColumnValue,
|
||||||
isAttributeColumnValue
|
isAttributeColumnValue
|
||||||
} from "@saleor/products/components/ProductListPage/utils";
|
} from "@saleor/products/components/ProductListPage/utils";
|
||||||
import { ProductListVariables } from "@saleor/products/types/ProductList";
|
import { ProductListVariables } from "@saleor/products/types/ProductList";
|
||||||
|
import useAttributeSearch from "@saleor/searches/useAttributeSearch";
|
||||||
import useCategorySearch from "@saleor/searches/useCategorySearch";
|
import useCategorySearch from "@saleor/searches/useCategorySearch";
|
||||||
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
|
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
|
||||||
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
||||||
|
@ -41,11 +45,13 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import ProductListPage from "../../components/ProductListPage";
|
import ProductListPage from "../../components/ProductListPage";
|
||||||
import {
|
import {
|
||||||
TypedProductBulkDeleteMutation,
|
TypedProductBulkDeleteMutation,
|
||||||
TypedProductBulkPublishMutation
|
TypedProductBulkPublishMutation,
|
||||||
|
useProductExport
|
||||||
} from "../../mutations";
|
} from "../../mutations";
|
||||||
import {
|
import {
|
||||||
AvailableInGridAttributesQuery,
|
AvailableInGridAttributesQuery,
|
||||||
TypedProductListQuery,
|
TypedProductListQuery,
|
||||||
|
useCountAllProducts,
|
||||||
useInitialProductFilterDataQuery
|
useInitialProductFilterDataQuery
|
||||||
} from "../../queries";
|
} from "../../queries";
|
||||||
import { productBulkDelete } from "../../types/productBulkDelete";
|
import { productBulkDelete } from "../../types/productBulkDelete";
|
||||||
|
@ -78,6 +84,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const paginate = usePaginator();
|
const paginate = usePaginator();
|
||||||
|
const { queue } = useBackgroundTask();
|
||||||
const shop = useShop();
|
const shop = useShop();
|
||||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||||
params.ids
|
params.ids
|
||||||
|
@ -111,6 +118,12 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
first: 5
|
first: 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const searchAttributes = useAttributeSearch({
|
||||||
|
variables: {
|
||||||
|
...DEFAULT_INITIAL_SEARCH_DATA,
|
||||||
|
first: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
() =>
|
() =>
|
||||||
|
@ -137,6 +150,29 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
ProductListUrlDialog,
|
ProductListUrlDialog,
|
||||||
ProductListUrlQueryParams
|
ProductListUrlQueryParams
|
||||||
>(navigate, productListUrl, params);
|
>(navigate, productListUrl, params);
|
||||||
|
const countAllProducts = useCountAllProducts({});
|
||||||
|
|
||||||
|
const [exportProducts, exportProductsOpts] = useProductExport({
|
||||||
|
onCompleted: data => {
|
||||||
|
if (data.exportProducts.errors.length === 0) {
|
||||||
|
notify({
|
||||||
|
text: intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address"
|
||||||
|
}),
|
||||||
|
title: intl.formatMessage({
|
||||||
|
defaultMessage: "Exporting CSV",
|
||||||
|
description: "waiting for export to end, header"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
queue(Task.EXPORT, {
|
||||||
|
id: data.exportProducts.exportFile.id
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [
|
const [
|
||||||
changeFilters,
|
changeFilters,
|
||||||
|
@ -398,6 +434,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
tabs={getFilterTabs().map(tab => tab.name)}
|
tabs={getFilterTabs().map(tab => tab.name)}
|
||||||
|
onExport={() => openModal("export")}
|
||||||
/>
|
/>
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
open={params.action === "delete"}
|
open={params.action === "delete"}
|
||||||
|
@ -493,6 +530,40 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
/>
|
/>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</ActionDialog>
|
</ActionDialog>
|
||||||
|
<ProductExportDialog
|
||||||
|
attributes={(
|
||||||
|
searchAttributes.result.data?.search.edges || []
|
||||||
|
).map(edge => edge.node)}
|
||||||
|
hasMore={
|
||||||
|
searchAttributes.result.data?.search.pageInfo
|
||||||
|
.hasNextPage
|
||||||
|
}
|
||||||
|
loading={searchAttributes.result.loading}
|
||||||
|
onFetch={searchAttributes.search}
|
||||||
|
onFetchMore={searchAttributes.loadMore}
|
||||||
|
open={params.action === "export"}
|
||||||
|
confirmButtonState={exportProductsOpts.status}
|
||||||
|
errors={
|
||||||
|
exportProductsOpts.data?.exportProducts.errors || []
|
||||||
|
}
|
||||||
|
productQuantity={{
|
||||||
|
all: countAllProducts.data?.products.totalCount,
|
||||||
|
filter: data?.products.totalCount
|
||||||
|
}}
|
||||||
|
selectedProducts={listElements.length}
|
||||||
|
onClose={closeModal}
|
||||||
|
onSubmit={data =>
|
||||||
|
exportProducts({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
...data,
|
||||||
|
filter,
|
||||||
|
ids: listElements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SaveFilterTabDialog
|
<SaveFilterTabDialog
|
||||||
open={params.action === "save-search"}
|
open={params.action === "save-search"}
|
||||||
confirmButtonState="default"
|
confirmButtonState="default"
|
||||||
|
|
42
src/searches/types/SearchAttributes.ts
Normal file
42
src/searches/types/SearchAttributes.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: SearchAttributes
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface SearchAttributes_search_edges_node {
|
||||||
|
__typename: "Attribute";
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAttributes_search_edges {
|
||||||
|
__typename: "AttributeCountableEdge";
|
||||||
|
node: SearchAttributes_search_edges_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAttributes_search_pageInfo {
|
||||||
|
__typename: "PageInfo";
|
||||||
|
endCursor: string | null;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
startCursor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAttributes_search {
|
||||||
|
__typename: "AttributeCountableConnection";
|
||||||
|
edges: SearchAttributes_search_edges[];
|
||||||
|
pageInfo: SearchAttributes_search_pageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAttributes {
|
||||||
|
search: SearchAttributes_search | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAttributesVariables {
|
||||||
|
after?: string | null;
|
||||||
|
first: number;
|
||||||
|
query: string;
|
||||||
|
}
|
33
src/searches/useAttributeSearch.ts
Normal file
33
src/searches/useAttributeSearch.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { pageInfoFragment } from "@saleor/fragments/pageInfo";
|
||||||
|
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SearchAttributes,
|
||||||
|
SearchAttributesVariables
|
||||||
|
} from "./types/SearchAttributes";
|
||||||
|
|
||||||
|
export const searchAttributes = gql`
|
||||||
|
${pageInfoFragment}
|
||||||
|
query SearchAttributes($after: String, $first: Int!, $query: String!) {
|
||||||
|
search: attributes(
|
||||||
|
after: $after
|
||||||
|
first: $first
|
||||||
|
filter: { search: $query }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
...PageInfoFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default makeTopLevelSearch<SearchAttributes, SearchAttributesVariables>(
|
||||||
|
searchAttributes
|
||||||
|
);
|
|
@ -48,6 +48,184 @@ exports[`Storyshots Discounts / Select countries default 1`] = `
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Generics / Accordion default 1`] = `
|
||||||
|
<div
|
||||||
|
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
|
||||||
|
style="margin:auto;overflow:visible;position:relative;width:400px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiCardContent-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-title-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiTypography-root-id Accordion-titleText-id MuiTypography-body1-id"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Accordion-expandButton-id"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Generics / Accordion opened 1`] = `
|
||||||
|
<div
|
||||||
|
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
|
||||||
|
style="margin:auto;overflow:visible;position:relative;width:400px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiCardContent-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-title-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiTypography-root-id Accordion-titleText-id MuiTypography-body1-id"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Accordion-expandButton-id"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19 13H5v-2h14v2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr
|
||||||
|
class="Hr-root-id"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Accordion-content-id"
|
||||||
|
>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Generics / Accordion with quick peek 1`] = `
|
||||||
|
<div
|
||||||
|
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
|
||||||
|
style="margin:auto;overflow:visible;position:relative;width:400px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiCardContent-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-root-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Accordion-title-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiTypography-root-id Accordion-titleText-id MuiTypography-body1-id"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Accordion-expandButton-id"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr
|
||||||
|
class="Hr-root-id"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Accordion-content-id"
|
||||||
|
>
|
||||||
|
Quick Peek
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Generics / Account Permission Groups Widget default 1`] = `
|
exports[`Storyshots Generics / Account Permission Groups Widget default 1`] = `
|
||||||
<div
|
<div
|
||||||
style="padding:24px"
|
style="padding:24px"
|
||||||
|
@ -113818,31 +113996,31 @@ exports[`Storyshots Views / Products / Create multiple variants interactive 1`]
|
||||||
class="Container-root-id"
|
class="Container-root-id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ProductVariantCreatorTabs-root-id"
|
class="CreatorSteps-root-id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ProductVariantCreatorTabs-tab-id ProductVariantCreatorTabs-tabActive-id ProductVariantCreatorTabs-tabVisited-id"
|
class="CreatorSteps-tab-id CreatorSteps-tabActive-id CreatorSteps-tabVisited-id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||||
>
|
>
|
||||||
Select Values
|
Select Values
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductVariantCreatorTabs-tab-id"
|
class="CreatorSteps-tab-id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||||
>
|
>
|
||||||
Prices and SKU
|
Prices and SKU
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductVariantCreatorTabs-tab-id"
|
class="CreatorSteps-tab-id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||||
>
|
>
|
||||||
Summary
|
Summary
|
||||||
</div>
|
</div>
|
||||||
|
@ -119378,6 +119556,24 @@ exports[`Storyshots Views / Products / Create product variant with errors 1`] =
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Views / Products / Export / Export settings errors 1`] = `
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Views / Products / Export / Export settings interactive 1`] = `
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Views / Products / Export / Export settings no products selected 1`] = `
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Views / Products / Product edit form errors 1`] = `
|
exports[`Storyshots Views / Products / Product edit form errors 1`] = `
|
||||||
<div
|
<div
|
||||||
style="padding:24px"
|
style="padding:24px"
|
||||||
|
@ -133368,6 +133564,33 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
|
||||||
<div
|
<div
|
||||||
class="PageHeader-root-id"
|
class="PageHeader-root-id"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-test="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="More"
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductListPage-columnPicker-id"
|
class="ProductListPage-columnPicker-id"
|
||||||
>
|
>
|
||||||
|
@ -135289,6 +135512,33 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
|
||||||
<div
|
<div
|
||||||
class="PageHeader-root-id"
|
class="PageHeader-root-id"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-test="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="More"
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductListPage-columnPicker-id"
|
class="ProductListPage-columnPicker-id"
|
||||||
>
|
>
|
||||||
|
@ -135788,6 +136038,33 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
|
||||||
<div
|
<div
|
||||||
class="PageHeader-root-id"
|
class="PageHeader-root-id"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-test="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="More"
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductListPage-columnPicker-id"
|
class="ProductListPage-columnPicker-id"
|
||||||
>
|
>
|
||||||
|
@ -136183,6 +136460,33 @@ exports[`Storyshots Views / Products / Product list not published 1`] = `
|
||||||
<div
|
<div
|
||||||
class="PageHeader-root-id"
|
class="PageHeader-root-id"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-test="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="More"
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductListPage-columnPicker-id"
|
class="ProductListPage-columnPicker-id"
|
||||||
>
|
>
|
||||||
|
@ -136964,6 +137268,33 @@ exports[`Storyshots Views / Products / Product list published 1`] = `
|
||||||
<div
|
<div
|
||||||
class="PageHeader-root-id"
|
class="PageHeader-root-id"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
data-test="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="More"
|
||||||
|
class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label-id"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root-id"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ProductListPage-columnPicker-id"
|
class="ProductListPage-columnPicker-id"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { formError } from "@saleor/storybook/misc";
|
|
||||||
import { storiesOf } from "@storybook/react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import ProductTypeAttributeEditDialog, {
|
|
||||||
ProductTypeAttributeEditDialogProps
|
|
||||||
} from "../../../productTypes/components/ProductTypeAttributeEditDialog";
|
|
||||||
import { attributes } from "../../../productTypes/fixtures";
|
|
||||||
import Decorator from "../../Decorator";
|
|
||||||
|
|
||||||
const attribute = attributes[0];
|
|
||||||
|
|
||||||
const props: ProductTypeAttributeEditDialogProps = {
|
|
||||||
disabled: false,
|
|
||||||
errors: [],
|
|
||||||
name: attribute.name,
|
|
||||||
onClose: () => undefined,
|
|
||||||
onConfirm: () => undefined,
|
|
||||||
opened: true,
|
|
||||||
title: "Add Attribute",
|
|
||||||
values: attribute.values.map(value => ({
|
|
||||||
label: value.name,
|
|
||||||
value: value.id
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
storiesOf("Product types / Edit attribute", module)
|
|
||||||
.addDecorator(Decorator)
|
|
||||||
.add("default", () => <ProductTypeAttributeEditDialog {...props} />)
|
|
||||||
.add("loading", () => (
|
|
||||||
<ProductTypeAttributeEditDialog {...props} disabled={true} />
|
|
||||||
))
|
|
||||||
.add("form errors", () => (
|
|
||||||
<ProductTypeAttributeEditDialog
|
|
||||||
{...props}
|
|
||||||
errors={["name", "values"].map(field => formError(field))}
|
|
||||||
/>
|
|
||||||
));
|
|
|
@ -39,6 +39,7 @@ const props: ProductListPageProps = {
|
||||||
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
||||||
filterOpts: productListFilterOpts,
|
filterOpts: productListFilterOpts,
|
||||||
gridAttributes: attributes,
|
gridAttributes: attributes,
|
||||||
|
onExport: () => undefined,
|
||||||
products,
|
products,
|
||||||
settings: {
|
settings: {
|
||||||
...pageListProps.default.settings,
|
...pageListProps.default.settings,
|
||||||
|
|
|
@ -401,6 +401,23 @@ export enum DiscountValueTypeEnum {
|
||||||
PERCENTAGE = "PERCENTAGE",
|
PERCENTAGE = "PERCENTAGE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ExportErrorCode {
|
||||||
|
INVALID = "INVALID",
|
||||||
|
NOT_FOUND = "NOT_FOUND",
|
||||||
|
REQUIRED = "REQUIRED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ExportScope {
|
||||||
|
ALL = "ALL",
|
||||||
|
FILTER = "FILTER",
|
||||||
|
IDS = "IDS",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FileTypesEnum {
|
||||||
|
CSV = "CSV",
|
||||||
|
XLSX = "XLSX",
|
||||||
|
}
|
||||||
|
|
||||||
export enum FulfillmentStatus {
|
export enum FulfillmentStatus {
|
||||||
CANCELED = "CANCELED",
|
CANCELED = "CANCELED",
|
||||||
FULFILLED = "FULFILLED",
|
FULFILLED = "FULFILLED",
|
||||||
|
@ -668,6 +685,23 @@ export enum ProductErrorCode {
|
||||||
VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT",
|
VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProductFieldEnum {
|
||||||
|
CATEGORY = "CATEGORY",
|
||||||
|
CHARGE_TAXES = "CHARGE_TAXES",
|
||||||
|
COLLECTIONS = "COLLECTIONS",
|
||||||
|
COST_PRICE = "COST_PRICE",
|
||||||
|
DESCRIPTION = "DESCRIPTION",
|
||||||
|
NAME = "NAME",
|
||||||
|
PRODUCT_IMAGES = "PRODUCT_IMAGES",
|
||||||
|
PRODUCT_TYPE = "PRODUCT_TYPE",
|
||||||
|
PRODUCT_WEIGHT = "PRODUCT_WEIGHT",
|
||||||
|
VARIANT_IMAGES = "VARIANT_IMAGES",
|
||||||
|
VARIANT_PRICE = "VARIANT_PRICE",
|
||||||
|
VARIANT_SKU = "VARIANT_SKU",
|
||||||
|
VARIANT_WEIGHT = "VARIANT_WEIGHT",
|
||||||
|
VISIBLE = "VISIBLE",
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProductOrderField {
|
export enum ProductOrderField {
|
||||||
DATE = "DATE",
|
DATE = "DATE",
|
||||||
MINIMAL_PRICE = "MINIMAL_PRICE",
|
MINIMAL_PRICE = "MINIMAL_PRICE",
|
||||||
|
@ -1079,6 +1113,20 @@ export interface DraftOrderInput {
|
||||||
customerNote?: string | null;
|
customerNote?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportInfoInput {
|
||||||
|
attributes?: string[] | null;
|
||||||
|
warehouses?: string[] | null;
|
||||||
|
fields?: ProductFieldEnum[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportProductsInput {
|
||||||
|
scope: ExportScope;
|
||||||
|
filter?: ProductFilterInput | null;
|
||||||
|
ids?: string[] | null;
|
||||||
|
exportInfo?: ExportInfoInput | null;
|
||||||
|
fileType: FileTypesEnum;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FulfillmentCancelInput {
|
export interface FulfillmentCancelInput {
|
||||||
warehouseId: string;
|
warehouseId: string;
|
||||||
}
|
}
|
||||||
|
|
24
src/utils/errors/export.ts
Normal file
24
src/utils/errors/export.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
|
||||||
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
import { ExportErrorCode } from "@saleor/types/globalTypes";
|
||||||
|
import { IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import commonErrorMessages from "./common";
|
||||||
|
|
||||||
|
function getExportErrorMessage(
|
||||||
|
err: Omit<ExportErrorFragment, "__typename"> | undefined,
|
||||||
|
intl: IntlShape
|
||||||
|
): string {
|
||||||
|
if (err) {
|
||||||
|
switch (err.code) {
|
||||||
|
case ExportErrorCode.REQUIRED:
|
||||||
|
return intl.formatMessage(commonMessages.requiredField);
|
||||||
|
default:
|
||||||
|
return intl.formatMessage(commonErrorMessages.unknownError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getExportErrorMessage;
|
|
@ -1,6 +1,7 @@
|
||||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||||
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
|
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
|
||||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||||
|
import { Node } from "@saleor/types";
|
||||||
|
|
||||||
export function mapCountriesToChoices(
|
export function mapCountriesToChoices(
|
||||||
countries: ShopInfo_shop_countries[]
|
countries: ShopInfo_shop_countries[]
|
||||||
|
@ -10,3 +11,12 @@ export function mapCountriesToChoices(
|
||||||
value: country.code
|
value: country.code
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapNodeToChoice(
|
||||||
|
nodes: Array<Node & Record<"name", string>>
|
||||||
|
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
|
||||||
|
return nodes.map(node => ({
|
||||||
|
label: node.name,
|
||||||
|
value: node.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import clone from "lodash-es/clone";
|
|
||||||
|
|
||||||
import { IFilter } from "@saleor/components/Filter";
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import clone from "lodash-es/clone";
|
||||||
|
|
||||||
export function getExistingKeys(o: object): string[] {
|
export function getExistingKeys(o: object): string[] {
|
||||||
return Object.keys(o).filter(key => o[key] !== undefined && o[key] !== null);
|
return Object.keys(o).filter(key => o[key] !== undefined && o[key] !== null);
|
||||||
|
|
Loading…
Reference in a new issue