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
|
||||
- Fix weight based rate update - #604 by @dominik-zeglen
|
||||
- Add product export - #620 by @dominik-zeglen
|
||||
|
||||
## 2.10.0
|
||||
|
||||
|
|
|
@ -1806,6 +1806,16 @@
|
|||
"context": "button",
|
||||
"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": {
|
||||
"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",
|
||||
"string": "Navigation"
|
||||
},
|
||||
"src_dot_nextStep": {
|
||||
"context": "go to next step, button",
|
||||
"string": "Next"
|
||||
},
|
||||
"src_dot_no": {
|
||||
"string": "No"
|
||||
},
|
||||
|
@ -3722,12 +3736,6 @@
|
|||
"src_dot_productTypes_dot_components_dot_AssignAttributeDialog_dot_902296540": {
|
||||
"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": {
|
||||
"context": "dialog header",
|
||||
"string": "Unassign Attribute From Product Type"
|
||||
|
@ -3915,6 +3923,145 @@
|
|||
"context": "product 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": {
|
||||
"context": "section header",
|
||||
"string": "All Photos"
|
||||
|
@ -3951,6 +4098,10 @@
|
|||
"context": "product 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": {
|
||||
"string": "Search Products..."
|
||||
},
|
||||
|
@ -4339,6 +4490,10 @@
|
|||
"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?}}"
|
||||
},
|
||||
"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": {
|
||||
"context": "publish product, button",
|
||||
"string": "Publish"
|
||||
|
@ -4367,6 +4522,9 @@
|
|||
"context": "dialog header",
|
||||
"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": {
|
||||
"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?}}"
|
||||
|
@ -4430,6 +4588,10 @@
|
|||
"src_dot_savedChanges": {
|
||||
"string": "Saved changes"
|
||||
},
|
||||
"src_dot_selectAll": {
|
||||
"context": "select all options, button",
|
||||
"string": "Select All"
|
||||
},
|
||||
"src_dot_send": {
|
||||
"context": "button",
|
||||
"string": "Send"
|
||||
|
|
121
schema.graphql
121
schema.graphql
|
@ -2232,6 +2232,108 @@ type Error {
|
|||
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 {
|
||||
id: ID!
|
||||
fulfillmentOrder: Int!
|
||||
|
@ -4657,6 +4759,23 @@ enum ProductErrorCode {
|
|||
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 {
|
||||
isPublished: Boolean
|
||||
collections: [ID]
|
||||
|
@ -5446,6 +5565,8 @@ type Query {
|
|||
first: Int
|
||||
last: Int
|
||||
): VoucherCountableConnection
|
||||
exportFile(id: ID!): ExportFile
|
||||
exportFiles(filter: ExportFileFilterInput, sortBy: ExportFileSortingInput, before: String, after: String, first: Int, last: Int): ExportFileCountableConnection
|
||||
taxTypes: [TaxType]
|
||||
checkout(token: UUID): Checkout
|
||||
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 Menu from "@material-ui/core/Menu";
|
||||
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 MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||
import React from "react";
|
||||
|
@ -9,6 +13,7 @@ const ITEM_HEIGHT = 48;
|
|||
|
||||
export interface CardMenuItem {
|
||||
label: string;
|
||||
testId?: string;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
|
@ -26,34 +31,58 @@ const useStyles = makeStyles(
|
|||
height: 32,
|
||||
padding: 0,
|
||||
width: 32
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(2),
|
||||
maxHeight: ITEM_HEIGHT * 4.5
|
||||
}
|
||||
}),
|
||||
{ name: "CardMenu" }
|
||||
);
|
||||
|
||||
const CardMenu: React.FC<CardMenuProps> = props => {
|
||||
const { className, disabled, menuItems } = props;
|
||||
const { className, disabled, menuItems, ...rest } = 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>) => {
|
||||
setAnchor(event.currentTarget);
|
||||
const handleToggle = () => setOpen(prevOpen => !prevOpen);
|
||||
|
||||
const handleClose = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (
|
||||
anchorRef.current &&
|
||||
anchorRef.current.contains(event.target as HTMLElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchor(null);
|
||||
const handleListKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuClick = (menuItemIndex: number) => {
|
||||
menuItems[menuItemIndex].onSelect();
|
||||
handleClose();
|
||||
};
|
||||
const prevOpen = React.useRef(open);
|
||||
React.useEffect(() => {
|
||||
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 (
|
||||
<div className={className}>
|
||||
<div className={className} {...rest}>
|
||||
<IconButton
|
||||
aria-label="More"
|
||||
aria-owns={open ? "long-menu" : null}
|
||||
|
@ -61,31 +90,47 @@ const CardMenu: React.FC<CardMenuProps> = props => {
|
|||
className={classes.iconButton}
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
ref={anchorRef}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="long-menu"
|
||||
anchorEl={anchorEl}
|
||||
<Popper
|
||||
placement="bottom-end"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5
|
||||
// width: 200
|
||||
}
|
||||
anchorEl={anchorRef.current}
|
||||
transition
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<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) => (
|
||||
<MenuItem
|
||||
onClick={() => handleMenuClick(menuItemIndex)}
|
||||
key={menuItem.label}
|
||||
data-test={menuItem.testId}
|
||||
>
|
||||
{menuItem.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,8 +23,8 @@ const useStyles = makeStyles(
|
|||
color: theme.palette.common.white
|
||||
},
|
||||
root: {
|
||||
background: fade(theme.palette.secondary.main, 0.8),
|
||||
borderRadius: 8,
|
||||
background: fade(theme.palette.primary.main, 0.8),
|
||||
borderRadius: 18,
|
||||
display: "inline-block",
|
||||
marginRight: theme.spacing(2),
|
||||
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 {
|
||||
value: string;
|
||||
export interface RadioGroupFieldChoice<
|
||||
T extends string | number = string | number
|
||||
> {
|
||||
disabled?: boolean;
|
||||
value: T;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
|
||||
|
@ -49,7 +52,7 @@ interface RadioGroupFieldProps {
|
|||
hint?: string;
|
||||
label?: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
value: string | number;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
|
@ -87,6 +90,7 @@ export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
|
|||
{choices.length > 0 ? (
|
||||
choices.map(choice => (
|
||||
<FormControlLabel
|
||||
disabled={choice.disabled}
|
||||
value={choice.value}
|
||||
className={classes.radioLabel}
|
||||
control={<Radio color="primary" />}
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface IMessage {
|
|||
autohide?: number;
|
||||
expandText?: string;
|
||||
title?: string;
|
||||
text: string;
|
||||
text: React.ReactNode;
|
||||
onUndo?: () => void;
|
||||
status?: Status;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,30 @@
|
|||
import { JobStatusEnum } from "@saleor/types/globalTypes";
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
import { createMockClient } from "mock-apollo-client";
|
||||
import { createMockClient, RequestHandlerResponse } from "mock-apollo-client";
|
||||
|
||||
import {
|
||||
backgroundTasksRefreshTime,
|
||||
useBackgroundTasks
|
||||
} 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();
|
||||
|
||||
function renderBackgroundTasks() {
|
||||
const mockClient = createMockClient();
|
||||
mockClient.setRequestHandler(checkExportFileStatus, () =>
|
||||
Promise.resolve<RequestHandlerResponse<CheckExportFileStatus>>({
|
||||
data: {
|
||||
exportFile: {
|
||||
__typename: "ExportFile",
|
||||
id: "123",
|
||||
status: JobStatusEnum.SUCCESS
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const intl = {
|
||||
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 => {
|
||||
const onCompleted = jest.fn();
|
||||
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { checkOrderInvoicesStatus } from "@saleor/orders/queries";
|
||||
import ApolloClient from "apollo-client";
|
||||
import React from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
|
||||
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";
|
||||
|
||||
export const backgroundTasksRefreshTime = 15 * 1000;
|
||||
|
@ -81,6 +86,22 @@ export function useBackgroundTasks(
|
|||
intl
|
||||
);
|
||||
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;
|
||||
|
|
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 { commonMessages } from "@saleor/intl";
|
||||
import { CheckOrderInvoicesStatus } from "@saleor/orders/types/CheckOrderInvoicesStatus";
|
||||
import { JobStatusEnum } from "@saleor/types/globalTypes";
|
||||
import { ApolloQueryResult } from "apollo-client";
|
||||
import { defineMessages, IntlShape } from "react-intl";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import messages from "./messages";
|
||||
import {
|
||||
InvoiceGenerateParams,
|
||||
QueuedTask,
|
||||
TaskData,
|
||||
TaskStatus
|
||||
} from "./types";
|
||||
import { CheckExportFileStatus } from "./types/CheckExportFileStatus";
|
||||
import { CheckOrderInvoicesStatus } from "./types/CheckOrderInvoicesStatus";
|
||||
|
||||
export const messages = defineMessages({
|
||||
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"
|
||||
function getTaskStatus(jobStatus: JobStatusEnum): TaskStatus {
|
||||
switch (jobStatus) {
|
||||
case JobStatusEnum.SUCCESS:
|
||||
return TaskStatus.SUCCESS;
|
||||
case JobStatusEnum.PENDING:
|
||||
return TaskStatus.PENDING;
|
||||
default:
|
||||
return TaskStatus.FAILURE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function handleTask(task: QueuedTask): Promise<TaskStatus> {
|
||||
let status = TaskStatus.PENDING;
|
||||
|
@ -89,11 +87,7 @@ export function queueInvoiceGenerate(
|
|||
invoice => invoice.id === generateInvoice.invoiceId
|
||||
).status;
|
||||
|
||||
return status === JobStatusEnum.SUCCESS
|
||||
? TaskStatus.SUCCESS
|
||||
: status === JobStatusEnum.PENDING
|
||||
? TaskStatus.PENDING
|
||||
: TaskStatus.FAILURE;
|
||||
return getTaskStatus(status);
|
||||
},
|
||||
id,
|
||||
onCompleted: data =>
|
||||
|
@ -104,6 +98,7 @@ export function queueInvoiceGenerate(
|
|||
title: intl.formatMessage(messages.invoiceGenerateFinishedTitle)
|
||||
})
|
||||
: notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong),
|
||||
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 {
|
||||
CUSTOM,
|
||||
EXPORT,
|
||||
INVOICE_GENERATE
|
||||
}
|
||||
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 */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { JobStatusEnum } from "./../../types/globalTypes";
|
||||
import { JobStatusEnum } from "./../../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: CheckOrderInvoicesStatus
|
|
@ -120,3 +120,10 @@ export const appErrorFragment = gql`
|
|||
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",
|
||||
description: "button"
|
||||
},
|
||||
nextStep: {
|
||||
defaultMessage: "Next",
|
||||
description: "go to next step, button"
|
||||
},
|
||||
ok: {
|
||||
defaultMessage: "OK",
|
||||
description: "button"
|
||||
|
@ -144,6 +148,10 @@ export const buttonMessages = defineMessages({
|
|||
defaultMessage: "Save",
|
||||
description: "button"
|
||||
},
|
||||
selectAll: {
|
||||
defaultMessage: "Select All",
|
||||
description: "select all options, button"
|
||||
},
|
||||
send: {
|
||||
defaultMessage: "Send",
|
||||
description: "button"
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import { fragmentAddress } from "@saleor/fragments/address";
|
||||
import {
|
||||
fragmentOrderDetails,
|
||||
invoiceFragment
|
||||
} from "@saleor/fragments/orders";
|
||||
import { fragmentOrderDetails } from "@saleor/fragments/orders";
|
||||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
|
||||
import gql from "graphql-tag";
|
||||
|
@ -231,15 +228,3 @@ export const useOrderFulfillData = makeQuery<
|
|||
OrderFulfillData,
|
||||
OrderFulfillDataVariables
|
||||
>(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 useNotifier from "@saleor/hooks/useNotifier";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
|
|
|
@ -30,7 +30,7 @@ import React from "react";
|
|||
import InfiniteScroll from "react-infinite-scroller";
|
||||
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(
|
||||
theme => ({
|
||||
|
@ -63,7 +63,7 @@ export interface AssignAttributeDialogProps extends FetchMoreProps {
|
|||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
errors: string[];
|
||||
open: boolean;
|
||||
attributes: SearchAttributes_productType_availableAttributes_edges_node[];
|
||||
attributes: SearchAvailableAttributes_productType_availableAttributes_edges_node[];
|
||||
selected: string[];
|
||||
onClose: () => 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 {
|
||||
SearchAttributes,
|
||||
SearchAttributesVariables
|
||||
} from "./types/SearchAttributes";
|
||||
SearchAvailableAttributes,
|
||||
SearchAvailableAttributesVariables
|
||||
} from "./types/SearchAvailableAttributes";
|
||||
|
||||
export const searchAttributes = gql`
|
||||
${pageInfoFragment}
|
||||
query SearchAttributes(
|
||||
query SearchAvailableAttributes(
|
||||
$id: ID!
|
||||
$after: String
|
||||
$first: Int!
|
||||
|
@ -37,9 +37,10 @@ export const searchAttributes = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export default makeSearch<SearchAttributes, SearchAttributesVariables>(
|
||||
searchAttributes,
|
||||
result =>
|
||||
export default makeSearch<
|
||||
SearchAvailableAttributes,
|
||||
SearchAvailableAttributesVariables
|
||||
>(searchAttributes, result =>
|
||||
result.loadMore(
|
||||
(prev, next) => {
|
||||
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 Card from "@material-ui/core/Card";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import CardMenu from "@saleor/components/CardMenu";
|
||||
import ColumnPicker, {
|
||||
ColumnPickerChoice
|
||||
} from "@saleor/components/ColumnPicker";
|
||||
|
@ -44,12 +45,13 @@ export interface ProductListPageProps
|
|||
gridAttributes: GridAttributes_grid_edges_node[];
|
||||
totalGridAttributes: number;
|
||||
products: ProductList_products_edges_node[];
|
||||
onExport: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
columnPicker: {
|
||||
marginRight: theme.spacing(3)
|
||||
margin: theme.spacing(0, 3)
|
||||
}
|
||||
}),
|
||||
{ name: "ProductListPage" }
|
||||
|
@ -71,6 +73,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
totalGridAttributes,
|
||||
onAdd,
|
||||
onAll,
|
||||
onExport,
|
||||
onFetchMore,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
|
@ -119,6 +122,19 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
return (
|
||||
<Container>
|
||||
<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
|
||||
className={classes.columnPicker}
|
||||
columns={columns}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import classNames from "classnames";
|
||||
import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
|
||||
import React from "react";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { ProductVariantCreatorStep } from "./types";
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
value: ProductVariantCreatorStep;
|
||||
}
|
||||
function getSteps(intl: IntlShape): Step[] {
|
||||
function useSteps(): Array<Step<ProductVariantCreatorStep>> {
|
||||
const intl = useIntl();
|
||||
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
|
@ -36,69 +32,27 @@ function getSteps(intl: IntlShape): Step[] {
|
|||
];
|
||||
}
|
||||
|
||||
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: "ProductVariantCreatorTabs"
|
||||
}
|
||||
);
|
||||
const ProductVariantCreatorSteps = makeCreatorSteps<
|
||||
ProductVariantCreatorStep
|
||||
>();
|
||||
|
||||
export interface ProductVariantCreatorTabsProps {
|
||||
step: ProductVariantCreatorStep;
|
||||
onStepClick: (step: ProductVariantCreatorStep) => void;
|
||||
}
|
||||
|
||||
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = props => {
|
||||
const { step: currentStep, onStepClick } = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const steps = getSteps(intl);
|
||||
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = ({
|
||||
step: currentStep,
|
||||
onStepClick
|
||||
}) => {
|
||||
const steps = useSteps();
|
||||
|
||||
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>
|
||||
<ProductVariantCreatorSteps
|
||||
currentStep={currentStep}
|
||||
steps={steps}
|
||||
onStepClick={onStepClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import {
|
||||
bulkProductErrorFragment,
|
||||
bulkStockErrorFragment,
|
||||
exportErrorFragment,
|
||||
productErrorFragment,
|
||||
stockErrorFragment
|
||||
} from "@saleor/fragments/errors";
|
||||
import {
|
||||
exportFileFragment,
|
||||
fragmentVariant,
|
||||
productFragmentDetails
|
||||
} from "@saleor/fragments/products";
|
||||
|
@ -22,6 +24,7 @@ import {
|
|||
} from "./types/productBulkPublish";
|
||||
import { ProductCreate, ProductCreateVariables } from "./types/ProductCreate";
|
||||
import { ProductDelete, ProductDeleteVariables } from "./types/ProductDelete";
|
||||
import { ProductExport, ProductExportVariables } from "./types/ProductExport";
|
||||
import {
|
||||
ProductImageCreate,
|
||||
ProductImageCreateVariables
|
||||
|
@ -567,3 +570,22 @@ export const TypedProductVariantBulkDeleteMutation = TypedMutation<
|
|||
ProductVariantBulkDelete,
|
||||
ProductVariantBulkDeleteVariables
|
||||
>(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 { TypedQuery } from "../queries";
|
||||
import { CountAllProducts } from "./types/CountAllProducts";
|
||||
import {
|
||||
CreateMultipleVariantsData,
|
||||
CreateMultipleVariantsDataVariables
|
||||
|
@ -145,6 +146,7 @@ const productListQuery = gql`
|
|||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -153,6 +155,17 @@ export const TypedProductListQuery = TypedQuery<
|
|||
ProductListVariables
|
||||
>(productListQuery);
|
||||
|
||||
const countAllProductsQuery = gql`
|
||||
query CountAllProducts {
|
||||
products {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const useCountAllProducts = makeQuery<CountAllProducts, null>(
|
||||
countAllProductsQuery
|
||||
);
|
||||
|
||||
const productDetailsQuery = gql`
|
||||
${productFragmentDetails}
|
||||
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";
|
||||
edges: ProductList_products_edges[];
|
||||
pageInfo: ProductList_products_pageInfo;
|
||||
totalCount: number | null;
|
||||
}
|
||||
|
||||
export interface ProductList {
|
||||
|
|
|
@ -23,6 +23,7 @@ export type ProductListUrlDialog =
|
|||
| "publish"
|
||||
| "unpublish"
|
||||
| "delete"
|
||||
| "export"
|
||||
| TabActionDialog;
|
||||
export enum ProductListUrlFiltersEnum {
|
||||
priceFrom = "priceFrom",
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
defaultListSettings,
|
||||
ProductListColumns
|
||||
} from "@saleor/config";
|
||||
import { Task } from "@saleor/containers/BackgroundTasks/types";
|
||||
import useBackgroundTask from "@saleor/hooks/useBackgroundTask";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
|
@ -23,11 +25,13 @@ import usePaginator, {
|
|||
import useShop from "@saleor/hooks/useShop";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { maybe } from "@saleor/misc";
|
||||
import ProductExportDialog from "@saleor/products/components/ProductExportDialog";
|
||||
import {
|
||||
getAttributeIdFromColumnValue,
|
||||
isAttributeColumnValue
|
||||
} from "@saleor/products/components/ProductListPage/utils";
|
||||
import { ProductListVariables } from "@saleor/products/types/ProductList";
|
||||
import useAttributeSearch from "@saleor/searches/useAttributeSearch";
|
||||
import useCategorySearch from "@saleor/searches/useCategorySearch";
|
||||
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
|
||||
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
||||
|
@ -41,11 +45,13 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
import ProductListPage from "../../components/ProductListPage";
|
||||
import {
|
||||
TypedProductBulkDeleteMutation,
|
||||
TypedProductBulkPublishMutation
|
||||
TypedProductBulkPublishMutation,
|
||||
useProductExport
|
||||
} from "../../mutations";
|
||||
import {
|
||||
AvailableInGridAttributesQuery,
|
||||
TypedProductListQuery,
|
||||
useCountAllProducts,
|
||||
useInitialProductFilterDataQuery
|
||||
} from "../../queries";
|
||||
import { productBulkDelete } from "../../types/productBulkDelete";
|
||||
|
@ -78,6 +84,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
const paginate = usePaginator();
|
||||
const { queue } = useBackgroundTask();
|
||||
const shop = useShop();
|
||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||
params.ids
|
||||
|
@ -111,6 +118,12 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
first: 5
|
||||
}
|
||||
});
|
||||
const searchAttributes = useAttributeSearch({
|
||||
variables: {
|
||||
...DEFAULT_INITIAL_SEARCH_DATA,
|
||||
first: 10
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
|
@ -137,6 +150,29 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
ProductListUrlDialog,
|
||||
ProductListUrlQueryParams
|
||||
>(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 [
|
||||
changeFilters,
|
||||
|
@ -398,6 +434,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
onTabChange={handleTabChange}
|
||||
initialSearch={params.query || ""}
|
||||
tabs={getFilterTabs().map(tab => tab.name)}
|
||||
onExport={() => openModal("export")}
|
||||
/>
|
||||
<ActionDialog
|
||||
open={params.action === "delete"}
|
||||
|
@ -493,6 +530,40 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
/>
|
||||
</DialogContentText>
|
||||
</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
|
||||
open={params.action === "save-search"}
|
||||
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`] = `
|
||||
<div
|
||||
style="padding:24px"
|
||||
|
@ -113818,31 +113996,31 @@ exports[`Storyshots Views / Products / Create multiple variants interactive 1`]
|
|||
class="Container-root-id"
|
||||
>
|
||||
<div
|
||||
class="ProductVariantCreatorTabs-root-id"
|
||||
class="CreatorSteps-root-id"
|
||||
>
|
||||
<div
|
||||
class="ProductVariantCreatorTabs-tab-id ProductVariantCreatorTabs-tabActive-id ProductVariantCreatorTabs-tabVisited-id"
|
||||
class="CreatorSteps-tab-id CreatorSteps-tabActive-id CreatorSteps-tabVisited-id"
|
||||
>
|
||||
<div
|
||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
||||
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||
>
|
||||
Select Values
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ProductVariantCreatorTabs-tab-id"
|
||||
class="CreatorSteps-tab-id"
|
||||
>
|
||||
<div
|
||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
||||
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||
>
|
||||
Prices and SKU
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ProductVariantCreatorTabs-tab-id"
|
||||
class="CreatorSteps-tab-id"
|
||||
>
|
||||
<div
|
||||
class="MuiTypography-root-id ProductVariantCreatorTabs-label-id MuiTypography-caption-id"
|
||||
class="MuiTypography-root-id CreatorSteps-label-id MuiTypography-caption-id"
|
||||
>
|
||||
Summary
|
||||
</div>
|
||||
|
@ -119378,6 +119556,24 @@ exports[`Storyshots Views / Products / Create product variant with errors 1`] =
|
|||
</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`] = `
|
||||
<div
|
||||
style="padding:24px"
|
||||
|
@ -133368,6 +133564,33 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
|
|||
<div
|
||||
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
|
||||
class="ProductListPage-columnPicker-id"
|
||||
>
|
||||
|
@ -135289,6 +135512,33 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
|
|||
<div
|
||||
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
|
||||
class="ProductListPage-columnPicker-id"
|
||||
>
|
||||
|
@ -135788,6 +136038,33 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
|
|||
<div
|
||||
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
|
||||
class="ProductListPage-columnPicker-id"
|
||||
>
|
||||
|
@ -136183,6 +136460,33 @@ exports[`Storyshots Views / Products / Product list not published 1`] = `
|
|||
<div
|
||||
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
|
||||
class="ProductListPage-columnPicker-id"
|
||||
>
|
||||
|
@ -136964,6 +137268,33 @@ exports[`Storyshots Views / Products / Product list published 1`] = `
|
|||
<div
|
||||
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
|
||||
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],
|
||||
filterOpts: productListFilterOpts,
|
||||
gridAttributes: attributes,
|
||||
onExport: () => undefined,
|
||||
products,
|
||||
settings: {
|
||||
...pageListProps.default.settings,
|
||||
|
|
|
@ -401,6 +401,23 @@ export enum DiscountValueTypeEnum {
|
|||
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 {
|
||||
CANCELED = "CANCELED",
|
||||
FULFILLED = "FULFILLED",
|
||||
|
@ -668,6 +685,23 @@ export enum ProductErrorCode {
|
|||
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 {
|
||||
DATE = "DATE",
|
||||
MINIMAL_PRICE = "MINIMAL_PRICE",
|
||||
|
@ -1079,6 +1113,20 @@ export interface DraftOrderInput {
|
|||
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 {
|
||||
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 { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { Node } from "@saleor/types";
|
||||
|
||||
export function mapCountriesToChoices(
|
||||
countries: ShopInfo_shop_countries[]
|
||||
|
@ -10,3 +11,12 @@ export function mapCountriesToChoices(
|
|||
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 clone from "lodash-es/clone";
|
||||
|
||||
export function getExistingKeys(o: object): string[] {
|
||||
return Object.keys(o).filter(key => o[key] !== undefined && o[key] !== null);
|
||||
|
|
Loading…
Reference in a new issue