Add warehouse choice

This commit is contained in:
dominik-zeglen 2020-08-13 13:24:05 +02:00
parent e57c081cce
commit 0b5c9e35eb
5 changed files with 308 additions and 79 deletions

View file

@ -3,6 +3,7 @@ import {
ExportErrorCode,
ExportProductsInput
} from "@saleor/types/globalTypes";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { storiesOf } from "@storybook/react";
import React from "react";
@ -30,7 +31,8 @@ const props: ProductExportDialogProps = {
all: 100,
filter: 32
},
selectedProducts: 18
selectedProducts: 18,
warehouses: warehouseList
};
storiesOf("Views / Products / Export / Export settings", module)

View file

@ -25,11 +25,13 @@ import {
import getExportErrorMessage from "@saleor/utils/errors/export";
import { toggle } from "@saleor/utils/lists";
import { mapNodeToChoice } from "@saleor/utils/maps";
import { WarehouseList_warehouses_edges_node } from "@saleor/warehouses/types/WarehouseList";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ProductExportDialogInfo, {
attributeNamePrefix
attributeNamePrefix,
warehouseNamePrefix
} from "./ProductExportDialogInfo";
import ProductExportDialogSettings, {
ProductQuantity
@ -64,7 +66,8 @@ function useSteps(): Array<Step<ProductExportStep>> {
const initialForm: ExportProductsInput = {
exportInfo: {
attributes: [],
fields: []
fields: [],
warehouses: []
},
fileType: FileTypesEnum.CSV,
scope: ExportScope.ALL
@ -78,6 +81,7 @@ export interface ProductExportDialogProps extends DialogProps, FetchMoreProps {
errors: ExportErrorFragment[];
productQuantity: ProductQuantity;
selectedProducts: number;
warehouses: WarehouseList_warehouses_edges_node[];
onFetch: (query: string) => void;
onSubmit: (data: ExportProductsInput) => void;
}
@ -91,6 +95,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
onSubmit,
open,
selectedProducts,
warehouses,
...fetchMoreProps
}) => {
const [step, { next, prev, set: setStep }] = useWizard(
@ -114,6 +119,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
});
const attributeChoices = mapNodeToChoice(attributes);
const warehouseChoices = mapNodeToChoice(warehouses);
const handleAttributeSelect: FormChange = event => {
const id = event.target.name.substr(attributeNamePrefix.length);
@ -135,6 +141,35 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
);
};
const handleWarehouseSelect: FormChange = event =>
change({
target: {
name: "exportInfo",
value: {
...data.exportInfo,
warehouses: toggle(
event.target.name.substr(warehouseNamePrefix.length),
data.exportInfo.warehouses,
(a, b) => a === b
)
}
}
});
const handleToggleAllWarehouses: FormChange = () =>
change({
target: {
name: "exportInfo",
value: {
...data.exportInfo,
warehouses:
data.exportInfo.warehouses.length === warehouses.length
? []
: warehouses.map(warehouse => warehouse.id)
}
}
});
return (
<Dialog onClose={onClose} open={open} maxWidth="sm" fullWidth>
<>
@ -156,7 +191,10 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
data={data}
selectedAttributes={selectedAttributes}
onAttrtibuteSelect={handleAttributeSelect}
onWarehouseSelect={handleWarehouseSelect}
onChange={change}
warehouses={warehouseChoices}
onSelectAllWarehouses={handleToggleAllWarehouses}
{...fetchMoreProps}
/>
)}

View file

@ -9,7 +9,7 @@ import Checkbox from "@saleor/components/Checkbox";
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 { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { sectionNames } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types";
@ -22,9 +22,18 @@ import React from "react";
import { useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import useProductExportFieldMessages from "./messages";
export const attributeNamePrefix = "attribute-";
export const warehouseNamePrefix = "warehouse-";
const maxChips = 4;
const inventoryFields = [
ProductFieldEnum.PRODUCT_WEIGHT,
ProductFieldEnum.VARIANT_SKU,
ProductFieldEnum.VARIANT_WEIGHT
];
const useStyles = makeStyles(
theme => ({
accordion: {
@ -45,12 +54,23 @@ const useStyles = makeStyles(
marginBottom: theme.spacing(3),
marginTop: theme.spacing(3)
},
hrWarehouses: {
marginBottom: theme.spacing(3),
marginTop: theme.spacing(1)
},
label: {
"&&": {
overflow: "visible"
},
"&:first-of-type": {
paddingTop: 0
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.divider}`
},
justifyContent: "space-between",
margin: theme.spacing(0),
padding: theme.spacing(1, 0),
width: "100%"
},
loadMoreContainer: {
@ -62,8 +82,14 @@ const useStyles = makeStyles(
display: "inline-block",
marginBottom: theme.spacing()
},
optionLabel: {
marginLeft: 0
},
quickPeekContainer: {
marginBottom: theme.spacing(-1)
},
warehousesLabel: {
marginBottom: theme.spacing(2)
}
}),
{
@ -80,6 +106,9 @@ const Option: React.FC<{
return (
<FormControlLabel
classes={{
label: classes.optionLabel
}}
color="primary"
control={
<Checkbox
@ -103,66 +132,7 @@ const FieldAccordion: React.FC<AccordionProps & {
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 getFieldLabel = useProductExportFieldMessages();
const selectedAll = fields.every(field =>
data.exportInfo.fields.includes(field)
@ -180,7 +150,7 @@ const FieldAccordion: React.FC<AccordionProps & {
{selectedFields.slice(0, maxChips).map(field => (
<Chip
className={classes.chip}
label={fieldNames[field]}
label={getFieldLabel(field)}
onClose={() =>
onChange({
target: {
@ -225,7 +195,7 @@ const FieldAccordion: React.FC<AccordionProps & {
onChange={onChange}
key={field}
>
{fieldNames[field]}
{getFieldLabel(field)}
</Option>
))}
</Accordion>
@ -234,11 +204,14 @@ const FieldAccordion: React.FC<AccordionProps & {
export interface ProductExportDialogInfoProps extends FetchMoreProps {
attributes: MultiAutocompleteChoiceType[];
warehouses: MultiAutocompleteChoiceType[];
data: ExportProductsInput;
selectedAttributes: MultiAutocompleteChoiceType[];
onAttrtibuteSelect: (event: ChangeEvent) => void;
onChange: (event: ChangeEvent) => void;
onAttrtibuteSelect: FormChange;
onWarehouseSelect: FormChange;
onChange: FormChange;
onFetch: (query: string) => void;
onSelectAllWarehouses: FormChange;
}
const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
@ -247,14 +220,18 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
hasMore,
selectedAttributes,
loading,
warehouses,
onAttrtibuteSelect,
onWarehouseSelect,
onChange,
onFetch,
onFetchMore
onFetchMore,
onSelectAllWarehouses
}) => {
const classes = useStyles({});
const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const getFieldLabel = useProductExportFieldMessages();
const handleFieldChange = (event: ChangeEvent) =>
onChange({
@ -289,6 +266,12 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
}
});
const selectedInventoryFields = data.exportInfo.fields.filter(field =>
inventoryFields.includes(field)
);
const selectedAllInventoryFields =
selectedInventoryFields.length === inventoryFields.length;
return (
<>
<Typography className={classes.dialogLabel}>
@ -409,22 +392,134 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
onToggleAll={handleToggleAllFields}
data-test="financial"
/>
<FieldAccordion
<Accordion
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}
quickPeek={
(data.exportInfo.warehouses.length > 0 ||
selectedInventoryFields.length > 0) && (
<div className={classes.quickPeekContainer}>
{selectedInventoryFields.slice(0, maxChips).map(field => (
<Chip
className={classes.chip}
label={getFieldLabel(field)}
onClose={() =>
onChange({
target: {
name: field,
value: false
}
})
}
/>
))}
{data.exportInfo.warehouses
.slice(0, maxChips - selectedInventoryFields.length)
.map(warehouseId => (
<Chip
className={classes.chip}
label={
warehouses.find(
warehouse => warehouse.value === warehouseId
).label
}
onClose={() =>
onWarehouseSelect({
target: {
name: warehouseNamePrefix + warehouseId,
value: undefined
}
})
}
/>
))}
{data.exportInfo.warehouses.length +
selectedInventoryFields.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:
data.exportInfo.warehouses.length +
selectedInventoryFields.length -
maxChips
}}
/>
</Typography>
)}
</div>
)
}
data-test="inventory"
/>
>
<div>
<Option
checked={selectedAllInventoryFields}
name="all"
onChange={() =>
handleToggleAllFields(
inventoryFields,
!selectedAllInventoryFields
)
}
>
<FormattedMessage
defaultMessage="Select All"
description="selectt all options"
/>
</Option>
{inventoryFields.map(field => (
<Option
checked={data.exportInfo.fields.includes(field)}
name={field}
onChange={handleFieldChange}
key={field}
>
{getFieldLabel(field)}
</Option>
))}
</div>
<Hr className={classes.hrWarehouses} />
<Typography>
<FormattedMessage defaultMessage="Export Product Stock Quantity to CSV" />
</Typography>
<div>
<Option
checked={warehouses.every(warehouse =>
data.exportInfo.warehouses.includes(warehouse.value)
)}
name="all-warehouses"
onChange={onSelectAllWarehouses}
>
<FormattedMessage
defaultMessage="Export stock for all warehouses"
description="option"
/>
</Option>
</div>
<Hr className={classes.hrWarehouses} />
<Typography className={classes.warehousesLabel} variant="subtitle1">
<FormattedMessage
defaultMessage="Warehouses A to Z"
description="list of warehouses"
/>
</Typography>
{warehouses.map(warehouse => (
<Option
checked={data.exportInfo.warehouses.includes(warehouse.value)}
name={warehouseNamePrefix + warehouse.value}
onChange={onWarehouseSelect}
key={warehouse.value}
>
{warehouse.label}
</Option>
))}
</Accordion>
<FieldAccordion
title={intl.formatMessage({
defaultMessage: "SEO Information",

View file

@ -0,0 +1,83 @@
import { ProductFieldEnum } from "@saleor/types/globalTypes";
import { useIntl } from "react-intl";
function useProductExportFieldMessages() {
const intl = useIntl();
const messages = {
[ProductFieldEnum.CATEGORY]: {
defaultMessage: "Category",
description: "product field",
id: "productExportFieldCategory"
},
[ProductFieldEnum.CHARGE_TAXES]: {
defaultMessage: "Charge Taxes",
description: "product field",
id: "productExportFieldTaxes"
},
[ProductFieldEnum.COLLECTIONS]: {
defaultMessage: "Collections",
description: "product field",
id: "productExportFieldCollections"
},
[ProductFieldEnum.COST_PRICE]: {
defaultMessage: "Cost Price",
description: "product field",
id: "productExportFieldPrice"
},
[ProductFieldEnum.DESCRIPTION]: {
defaultMessage: "Description",
description: "product field",
id: "productExportFieldDescription"
},
[ProductFieldEnum.NAME]: {
defaultMessage: "Name",
description: "product field",
id: "productExportFieldName"
},
[ProductFieldEnum.PRODUCT_IMAGES]: {
defaultMessage: "Product Images",
description: "product field",
id: "productExportFieldProductImages"
},
[ProductFieldEnum.PRODUCT_TYPE]: {
defaultMessage: "Type",
description: "product field",
id: "productExportFieldType"
},
[ProductFieldEnum.PRODUCT_WEIGHT]: {
defaultMessage: "Export Product Weight",
description: "product field",
id: "productExportFieldProductWeight"
},
[ProductFieldEnum.VARIANT_IMAGES]: {
defaultMessage: "Variant Images",
description: "product field",
id: "productExportFieldVariantImages"
},
[ProductFieldEnum.VARIANT_PRICE]: {
defaultMessage: "Variant Price",
description: "product field",
id: "productExportFieldVariantPrice"
},
[ProductFieldEnum.VARIANT_SKU]: {
defaultMessage: "Export Variant SKU",
description: "product field",
id: "productExportFieldVariantSku"
},
[ProductFieldEnum.VARIANT_WEIGHT]: {
defaultMessage: "Export Variant Weight",
description: "product field",
id: "productExportFieldVariantWeight"
},
[ProductFieldEnum.VISIBLE]: {
defaultMessage: "Visibility",
description: "product field",
id: "productExportFieldVisibility"
}
};
return (field: ProductFieldEnum) => intl.formatMessage(messages[field]);
}
export default useProductExportFieldMessages;

View file

@ -39,6 +39,7 @@ import { ListViews } from "@saleor/types";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import { getSortUrlVariables } from "@saleor/utils/sort";
import { useWarehouseList } from "@saleor/warehouses/queries";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -124,6 +125,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
first: 10
}
});
const warehouses = useWarehouseList({
variables: {
first: 100
}
});
React.useEffect(
() =>
@ -551,6 +557,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
filter: data?.products.totalCount
}}
selectedProducts={listElements.length}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
onClose={closeModal}
onSubmit={data =>
exportProducts({