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

View file

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

View file

@ -9,7 +9,7 @@ import Checkbox from "@saleor/components/Checkbox";
import Chip from "@saleor/components/Chip"; import Chip from "@saleor/components/Chip";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; 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 useSearchQuery from "@saleor/hooks/useSearchQuery";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
@ -22,9 +22,18 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import useProductExportFieldMessages from "./messages";
export const attributeNamePrefix = "attribute-"; export const attributeNamePrefix = "attribute-";
export const warehouseNamePrefix = "warehouse-";
const maxChips = 4; const maxChips = 4;
const inventoryFields = [
ProductFieldEnum.PRODUCT_WEIGHT,
ProductFieldEnum.VARIANT_SKU,
ProductFieldEnum.VARIANT_WEIGHT
];
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
accordion: { accordion: {
@ -45,12 +54,23 @@ const useStyles = makeStyles(
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),
marginTop: theme.spacing(3) marginTop: theme.spacing(3)
}, },
hrWarehouses: {
marginBottom: theme.spacing(3),
marginTop: theme.spacing(1)
},
label: { label: {
"&&": { "&&": {
overflow: "visible" overflow: "visible"
}, },
"&:first-of-type": {
paddingTop: 0
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.divider}`
},
justifyContent: "space-between", justifyContent: "space-between",
margin: theme.spacing(0), margin: theme.spacing(0),
padding: theme.spacing(1, 0),
width: "100%" width: "100%"
}, },
loadMoreContainer: { loadMoreContainer: {
@ -62,8 +82,14 @@ const useStyles = makeStyles(
display: "inline-block", display: "inline-block",
marginBottom: theme.spacing() marginBottom: theme.spacing()
}, },
optionLabel: {
marginLeft: 0
},
quickPeekContainer: { quickPeekContainer: {
marginBottom: theme.spacing(-1) marginBottom: theme.spacing(-1)
},
warehousesLabel: {
marginBottom: theme.spacing(2)
} }
}), }),
{ {
@ -80,6 +106,9 @@ const Option: React.FC<{
return ( return (
<FormControlLabel <FormControlLabel
classes={{
label: classes.optionLabel
}}
color="primary" color="primary"
control={ control={
<Checkbox <Checkbox
@ -103,66 +132,7 @@ const FieldAccordion: React.FC<AccordionProps & {
onToggleAll: (field: ProductFieldEnum[], setTo: boolean) => void; onToggleAll: (field: ProductFieldEnum[], setTo: boolean) => void;
}> = ({ data, fields, onChange, onToggleAll, ...props }) => { }> = ({ data, fields, onChange, onToggleAll, ...props }) => {
const classes = useStyles({}); const classes = useStyles({});
const intl = useIntl(); const getFieldLabel = useProductExportFieldMessages();
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 => const selectedAll = fields.every(field =>
data.exportInfo.fields.includes(field) data.exportInfo.fields.includes(field)
@ -180,7 +150,7 @@ const FieldAccordion: React.FC<AccordionProps & {
{selectedFields.slice(0, maxChips).map(field => ( {selectedFields.slice(0, maxChips).map(field => (
<Chip <Chip
className={classes.chip} className={classes.chip}
label={fieldNames[field]} label={getFieldLabel(field)}
onClose={() => onClose={() =>
onChange({ onChange({
target: { target: {
@ -225,7 +195,7 @@ const FieldAccordion: React.FC<AccordionProps & {
onChange={onChange} onChange={onChange}
key={field} key={field}
> >
{fieldNames[field]} {getFieldLabel(field)}
</Option> </Option>
))} ))}
</Accordion> </Accordion>
@ -234,11 +204,14 @@ const FieldAccordion: React.FC<AccordionProps & {
export interface ProductExportDialogInfoProps extends FetchMoreProps { export interface ProductExportDialogInfoProps extends FetchMoreProps {
attributes: MultiAutocompleteChoiceType[]; attributes: MultiAutocompleteChoiceType[];
warehouses: MultiAutocompleteChoiceType[];
data: ExportProductsInput; data: ExportProductsInput;
selectedAttributes: MultiAutocompleteChoiceType[]; selectedAttributes: MultiAutocompleteChoiceType[];
onAttrtibuteSelect: (event: ChangeEvent) => void; onAttrtibuteSelect: FormChange;
onChange: (event: ChangeEvent) => void; onWarehouseSelect: FormChange;
onChange: FormChange;
onFetch: (query: string) => void; onFetch: (query: string) => void;
onSelectAllWarehouses: FormChange;
} }
const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
@ -247,14 +220,18 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
hasMore, hasMore,
selectedAttributes, selectedAttributes,
loading, loading,
warehouses,
onAttrtibuteSelect, onAttrtibuteSelect,
onWarehouseSelect,
onChange, onChange,
onFetch, onFetch,
onFetchMore onFetchMore,
onSelectAllWarehouses
}) => { }) => {
const classes = useStyles({}); const classes = useStyles({});
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const getFieldLabel = useProductExportFieldMessages();
const handleFieldChange = (event: ChangeEvent) => const handleFieldChange = (event: ChangeEvent) =>
onChange({ 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 ( return (
<> <>
<Typography className={classes.dialogLabel}> <Typography className={classes.dialogLabel}>
@ -409,22 +392,134 @@ const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
onToggleAll={handleToggleAllFields} onToggleAll={handleToggleAllFields}
data-test="financial" data-test="financial"
/> />
<FieldAccordion <Accordion
className={classes.accordion} className={classes.accordion}
title={intl.formatMessage({ title={intl.formatMessage({
defaultMessage: "Inventory Information", defaultMessage: "Inventory Information",
description: "informations about product stock, header" description: "informations about product stock, header"
})} })}
data={data} quickPeek={
fields={[ (data.exportInfo.warehouses.length > 0 ||
ProductFieldEnum.PRODUCT_WEIGHT, selectedInventoryFields.length > 0) && (
ProductFieldEnum.VARIANT_SKU, <div className={classes.quickPeekContainer}>
ProductFieldEnum.VARIANT_WEIGHT {selectedInventoryFields.slice(0, maxChips).map(field => (
]} <Chip
onChange={handleFieldChange} className={classes.chip}
onToggleAll={handleToggleAllFields} 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" 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 <FieldAccordion
title={intl.formatMessage({ title={intl.formatMessage({
defaultMessage: "SEO Information", 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 createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import { getSortUrlVariables } from "@saleor/utils/sort"; import { getSortUrlVariables } from "@saleor/utils/sort";
import { useWarehouseList } from "@saleor/warehouses/queries";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -124,6 +125,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
first: 10 first: 10
} }
}); });
const warehouses = useWarehouseList({
variables: {
first: 100
}
});
React.useEffect( React.useEffect(
() => () =>
@ -551,6 +557,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
filter: data?.products.totalCount filter: data?.products.totalCount
}} }}
selectedProducts={listElements.length} selectedProducts={listElements.length}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
onClose={closeModal} onClose={closeModal}
onSubmit={data => onSubmit={data =>
exportProducts({ exportProducts({