Add stock to variant matrix

This commit is contained in:
dominik-zeglen 2020-04-03 16:29:32 +02:00
parent 422ede89a8
commit e153b59bc3
18 changed files with 395 additions and 252 deletions

View file

@ -4643,7 +4643,7 @@ input ProductVariantBulkCreateInput {
costPrice: Decimal costPrice: Decimal
priceOverride: Decimal priceOverride: Decimal
sku: String! sku: String!
quantity: Int stocks: [StockInput!]!
trackInventory: Boolean trackInventory: Boolean
weight: WeightScalar weight: WeightScalar
} }

View file

@ -1,19 +1,33 @@
import { useState } from "react"; import { useState } from "react";
export interface UseWizardOpts<T> { export interface UseWizardActions<T> {
next: () => void; next: () => void;
prev: () => void; prev: () => void;
set: (step: T) => void; set: (step: T) => void;
} }
export type UseWizard<T> = [T, UseWizardOpts<T>]; export interface UseWizardOpts<T> {
function useWizard<T>(initial: T, steps: T[]): UseWizard<T> { onTransition: (prevStep: T, nextStep: T) => void;
}
export type UseWizard<T> = [T, UseWizardActions<T>];
function useWizard<T>(
initial: T,
steps: T[],
opts?: UseWizardOpts<T>
): UseWizard<T> {
const [stepIndex, setStepIndex] = useState(steps.indexOf(initial)); const [stepIndex, setStepIndex] = useState(steps.indexOf(initial));
function goToStep(nextStepIndex) {
if (typeof opts?.onTransition === "function") {
opts.onTransition(steps[stepIndex], steps[nextStepIndex]);
}
setStepIndex(nextStepIndex);
}
function next() { function next() {
if (stepIndex === steps.length - 1) { if (stepIndex === steps.length - 1) {
console.error("This is the last step"); console.error("This is the last step");
} else { } else {
setStepIndex(stepIndex + 1); goToStep(stepIndex + 1);
} }
} }
@ -21,7 +35,7 @@ function useWizard<T>(initial: T, steps: T[]): UseWizard<T> {
if (stepIndex === 0) { if (stepIndex === 0) {
console.error("This is the first step"); console.error("This is the first step");
} else { } else {
setStepIndex(stepIndex - 1); goToStep(stepIndex - 1);
} }
} }
@ -30,7 +44,7 @@ function useWizard<T>(initial: T, steps: T[]): UseWizard<T> {
if (newStepIndex === -1) { if (newStepIndex === -1) {
console.error("Step does not exist"); console.error("Step does not exist");
} else { } else {
setStepIndex(newStepIndex); goToStep(newStepIndex);
} }
} }

View file

@ -5,9 +5,10 @@ import { attributes } from "@saleor/attributes/fixtures";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate"; import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { ProductErrorCode } from "@saleor/types/globalTypes"; import { ProductErrorCode } from "@saleor/types/globalTypes";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import { warehouseList } from "@saleor/warehouses/fixtures";
import Decorator from "../../../storybook/Decorator"; import Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants"; import { createVariants } from "./createVariants";
import { AllOrAttribute } from "./form"; import { AllOrAttribute, ProductVariantCreateFormData } from "./form";
import ProductVariantCreatorContent, { import ProductVariantCreatorContent, {
ProductVariantCreatorContentProps ProductVariantCreatorContentProps
} from "./ProductVariantCreatorContent"; } from "./ProductVariantCreatorContent";
@ -15,24 +16,33 @@ import ProductVariantCreatorPage from "./ProductVariantCreatorPage";
import { ProductVariantCreatorStep } from "./types"; import { ProductVariantCreatorStep } from "./types";
const selectedAttributes = [1, 4, 5].map(index => attributes[index]); const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
const selectedWarehouses = [0, 1, 3].map(index => warehouseList[index]);
const price: AllOrAttribute = { const price: AllOrAttribute<string> = {
all: false, all: false,
attribute: selectedAttributes[1].id, attribute: selectedAttributes[0].id,
value: "2.79", value: "2.79",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({
slug: attribute.slug, slug: attribute.slug,
value: (attributeIndex + 4).toFixed(2) value: (attributeIndex + 4).toFixed(2)
})) }))
}; };
const stock: AllOrAttribute = { const stock: AllOrAttribute<string[]> = {
all: false, all: false,
attribute: selectedAttributes[1].id, attribute: selectedAttributes[0].id,
value: "8", value: selectedWarehouses.map((_, warehouseIndex) =>
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ ((warehouseIndex + 2) * 3).toString()
),
values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({
slug: attribute.slug, slug: attribute.slug,
value: (selectedAttributes.length * 10 - attributeIndex).toString() value: selectedWarehouses.map((_, warehouseIndex) =>
(
selectedAttributes.length * 10 -
attributeIndex -
warehouseIndex * 3
).toString()
)
})) }))
}; };
@ -52,23 +62,30 @@ const errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] = [
} }
]; ];
const data: ProductVariantCreateFormData = {
attributes: dataAttributes,
price,
stock,
variants: createVariants({
attributes: dataAttributes,
price,
stock,
variants: [],
warehouses: selectedWarehouses.map(warehouse => warehouse.id)
}),
warehouses: selectedWarehouses.map(warehouse => warehouse.id)
};
const props: ProductVariantCreatorContentProps = { const props: ProductVariantCreatorContentProps = {
attributes, attributes,
currencySymbol: "USD", currencySymbol: "USD",
data: { data: {
attributes: dataAttributes, ...data,
price, variants: createVariants(data)
stock,
variants: createVariants({
attributes: dataAttributes,
price,
stock,
variants: []
})
}, },
dispatchFormDataAction: () => undefined, dispatchFormDataAction: () => undefined,
errors: [], errors: [],
step: ProductVariantCreatorStep.values step: ProductVariantCreatorStep.values,
warehouses: warehouseList
}; };
storiesOf("Views / Products / Create multiple variants", module) storiesOf("Views / Products / Create multiple variants", module)

View file

@ -3,6 +3,7 @@ import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate"; import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { isSelected } from "@saleor/utils/lists"; import { isSelected } from "@saleor/utils/lists";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
import ProductVariantCreatePrices from "./ProductVariantCreatorPrices"; import ProductVariantCreatePrices from "./ProductVariantCreatorPrices";
import ProductVariantCreateSummary from "./ProductVariantCreatorSummary"; import ProductVariantCreateSummary from "./ProductVariantCreatorSummary";
@ -17,6 +18,7 @@ export interface ProductVariantCreatorContentProps {
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>; dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[]; errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
step: ProductVariantCreatorStep; step: ProductVariantCreatorStep;
warehouses: WarehouseFragment[];
} }
const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps> = props => { const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps> = props => {
@ -26,7 +28,8 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
data, data,
dispatchFormDataAction, dispatchFormDataAction,
errors, errors,
step step,
warehouses
} = props; } = props;
const selectedAttributes = attributes.filter(attribute => const selectedAttributes = attributes.filter(attribute =>
isSelected( isSelected(
@ -106,12 +109,23 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
variantIndex variantIndex
}) })
} }
onVariantStockDataChange={(variantIndex, warehouse, value) =>
dispatchFormDataAction({
stock: {
quantity: parseInt(value, 10),
warehouse
},
type: "changeVariantStockData",
variantIndex
})
}
onVariantDelete={variantIndex => onVariantDelete={variantIndex =>
dispatchFormDataAction({ dispatchFormDataAction({
type: "deleteVariant", type: "deleteVariant",
variantIndex variantIndex
}) })
} }
warehouses={warehouses}
/> />
)} )}
</> </>

View file

@ -50,17 +50,8 @@ function canHitNext(
} }
} }
if (data.stock.all) { if (!data.stock.all || data.stock.attribute) {
if (data.stock.value === "") { return false;
return false;
}
} else {
if (
data.stock.attribute === "" ||
data.stock.values.some(attributeValue => attributeValue.value === "")
) {
return false;
}
} }
return true; return true;
@ -102,25 +93,42 @@ function getTitle(step: ProductVariantCreatorStep, intl: IntlShape): string {
} }
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props => { const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props => {
const { attributes, defaultPrice, errors, onSubmit, ...contentProps } = props; const {
attributes,
defaultPrice,
errors,
onSubmit,
warehouses,
...contentProps
} = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const [step, { next: nextStep, prev: prevStep, set: setStep }] = useWizard<
ProductVariantCreatorStep
>(ProductVariantCreatorStep.values, [
ProductVariantCreatorStep.values,
ProductVariantCreatorStep.prices,
ProductVariantCreatorStep.summary
]);
const [wizardData, dispatchFormDataAction] = React.useReducer( const [wizardData, dispatchFormDataAction] = React.useReducer(
reduceProductVariantCreateFormData, reduceProductVariantCreateFormData,
createInitialForm(attributes, defaultPrice) createInitialForm(attributes, defaultPrice, warehouses)
);
const [step, { next: nextStep, prev: prevStep, set: setStep }] = useWizard<
ProductVariantCreatorStep
>(
ProductVariantCreatorStep.values,
[
ProductVariantCreatorStep.values,
ProductVariantCreatorStep.prices,
ProductVariantCreatorStep.summary
],
{
onTransition: (_, nextStep) => {
if (nextStep === ProductVariantCreatorStep.summary) {
dispatchFormDataAction({
type: "reload"
});
}
}
}
); );
const reloadForm = () => const reloadForm = () =>
dispatchFormDataAction({ dispatchFormDataAction({
data: createInitialForm(attributes, defaultPrice), data: createInitialForm(attributes, defaultPrice, warehouses),
type: "reload" type: "reload"
}); });
@ -170,6 +178,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
dispatchFormDataAction={dispatchFormDataAction} dispatchFormDataAction={dispatchFormDataAction}
errors={errors} errors={errors}
step={step} step={step}
warehouses={warehouses}
/> />
</Container> </Container>
); );

View file

@ -5,7 +5,7 @@ import purple from "@material-ui/core/colors/purple";
import yellow from "@material-ui/core/colors/yellow"; import yellow from "@material-ui/core/colors/yellow";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles, Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import classNames from "classnames"; import classNames from "classnames";
@ -17,7 +17,8 @@ import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
import { getFormErrors } from "@saleor/utils/errors"; import { getFormErrors } from "@saleor/utils/errors";
import { getBulkProductErrorMessage } from "@saleor/utils/errors/product"; import { getBulkProductErrorMessage } from "@saleor/utils/errors/product";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import { commonMessages } from "@saleor/intl"; import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import Hr from "@saleor/components/Hr";
import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
import { VariantField } from "./reducer"; import { VariantField } from "./reducer";
@ -27,39 +28,62 @@ export interface ProductVariantCreatorSummaryProps {
currencySymbol: string; currencySymbol: string;
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[]; errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
warehouses: WarehouseFragment[];
onVariantDataChange: ( onVariantDataChange: (
variantIndex: number, variantIndex: number,
field: VariantField, field: VariantField,
value: string value: string
) => void; ) => void;
onVariantStockDataChange: (
variantIndex: number,
warehouseId: string,
value: string
) => void;
onVariantDelete: (variantIndex: number) => void; onVariantDelete: (variantIndex: number) => void;
} }
type ClassKey =
| "attributeValue"
| "card"
| "col"
| "colHeader"
| "colName"
| "colPrice"
| "colSku"
| "colStock"
| "delete"
| "hr"
| "input"
| "summary";
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
const useStyles = makeStyles( const useStyles = makeStyles<
Theme,
ProductVariantCreatorSummaryProps,
ClassKey
>(
theme => ({ theme => ({
attributeValue: { attributeValue: {
display: "inline-block", display: "inline-block",
marginRight: theme.spacing(1) marginRight: theme.spacing(1)
}, },
col: { card: {
...theme.typography.body1, paddingBottom: theme.spacing()
fontSize: 14,
paddingLeft: theme.spacing(),
paddingRight: theme.spacing(1)
}, },
colHeader: { col: {
...theme.typography.body1, ...theme.typography.body1,
fontSize: 14 fontSize: 14
}, },
colHeader: {
...theme.typography.body1,
fontSize: 14,
paddingTop: theme.spacing(3)
},
colName: { colName: {
"&&": {
paddingLeft: "0 !important"
},
"&:not($colHeader)": { "&:not($colHeader)": {
paddingTop: theme.spacing(2) paddingTop: theme.spacing(2)
} },
paddingLeft: theme.spacing(3)
}, },
colPrice: {}, colPrice: {},
colSku: {}, colSku: {},
@ -67,22 +91,21 @@ const useStyles = makeStyles(
delete: { delete: {
marginTop: theme.spacing(0.5) marginTop: theme.spacing(0.5)
}, },
errorRow: {},
hr: { hr: {
marginBottom: theme.spacing(), gridColumn: props => `span ${4 + props.data.stock.value.length}`
marginTop: theme.spacing(0.5)
}, },
input: { input: {
"& input": { "& input": {
padding: "16px 12px 17px" padding: "16px 12px 17px"
}, }
marginTop: theme.spacing(0.5)
}, },
row: { summary: {
borderBottom: `1px solid ${theme.palette.divider}`, columnGap: theme.spacing(3),
display: "grid", display: "grid",
gridTemplateColumns: "1fr 180px 120px 180px 64px", gridTemplateColumns: props =>
padding: theme.spacing(1, 1, 1, 3) `minmax(240px, auto) 170px repeat(${props.data.stock.value.length}, 140px) 140px 64px`,
overflowX: "scroll",
rowGap: theme.spacing() + "px"
} }
}), }),
{ {
@ -115,63 +138,66 @@ const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps>
currencySymbol, currencySymbol,
data, data,
errors, errors,
warehouses,
onVariantDataChange, onVariantDataChange,
onVariantDelete onVariantDelete,
onVariantStockDataChange
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
return ( return (
<Card> <Card className={classes.card}>
<CardTitle title={intl.formatMessage(commonMessages.summary)} /> <CardTitle
<div> title={intl.formatMessage({
<div className={classes.row}> defaultMessage: "Created Variants",
<div description: "variant creator summary card header"
className={classNames( })}
classes.col, />
classes.colHeader, <div className={classes.summary}>
classes.colName <div
)} className={classNames(
> classes.col,
<FormattedMessage classes.colHeader,
defaultMessage="Variant" classes.colName
description="variant name" )}
/> >
</div> <FormattedMessage
<div defaultMessage="Variant"
className={classNames( description="variant name"
classes.col, />
classes.colHeader, </div>
classes.colPrice <div
)} className={classNames(
> classes.col,
<FormattedMessage classes.colHeader,
defaultMessage="Price" classes.colPrice
description="variant price" )}
/> >
</div> <FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</div>
{data.warehouses.map(warehouseId => (
<div <div
className={classNames( className={classNames(
classes.col, classes.col,
classes.colHeader, classes.colHeader,
classes.colStock classes.colStock
)} )}
key={warehouseId}
> >
<FormattedMessage {warehouses.find(warehouse => warehouse.id === warehouseId).name}
defaultMessage="Inventory"
description="variant stock amount"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colSku
)}
>
<FormattedMessage defaultMessage="SKU" />
</div> </div>
))}
<div
className={classNames(classes.col, classes.colHeader, classes.colSku)}
>
<FormattedMessage defaultMessage="SKU" />
</div> </div>
<div className={classNames(classes.col, classes.colHeader)} />
<Hr className={classes.hr} />
{data.variants.map((variant, variantIndex) => { {data.variants.map((variant, variantIndex) => {
const variantErrors = errors.filter( const variantErrors = errors.filter(
error => error.index === variantIndex error => error.index === variantIndex
@ -182,10 +208,7 @@ const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps>
); );
return ( return (
<div <React.Fragment
className={classNames(classes.row, {
[classes.errorRow]: variantErrors.length > 0
})}
key={variant.attributes key={variant.attributes
.map(attribute => attribute.values[0]) .map(attribute => attribute.values[0])
.join(":")} .join(":")}
@ -198,7 +221,7 @@ const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps>
style={{ style={{
color: colors[valueIndex % colors.length] color: colors[valueIndex % colors.length]
}} }}
key={value} key={`${value}:${valueIndex}`}
> >
{value} {value}
</span> </span>
@ -231,29 +254,34 @@ const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps>
} }
/> />
</div> </div>
<div className={classNames(classes.col, classes.colStock)}> {variant.stocks.map(stock => (
<TextField <div
className={classes.input} className={classNames(classes.col, classes.colStock)}
error={!!variantFormErrors.quantity} key={stock.warehouse}
helperText={getBulkProductErrorMessage( >
variantFormErrors.quantity, <TextField
intl className={classes.input}
)} error={!!variantFormErrors.quantity}
inputProps={{ helperText={getBulkProductErrorMessage(
min: 0, variantFormErrors.quantity,
type: "number" intl
}} )}
fullWidth inputProps={{
value={variant.quantity} min: 0,
onChange={event => type: "number"
onVariantDataChange( }}
variantIndex, fullWidth
"stock", value={stock.quantity}
event.target.value onChange={event =>
) onVariantStockDataChange(
} variantIndex,
/> stock.warehouse,
</div> event.target.value
)
}
/>
</div>
))}
<div className={classNames(classes.col, classes.colSku)}> <div className={classNames(classes.col, classes.colSku)}>
<TextField <TextField
className={classes.input} className={classes.input}
@ -278,7 +306,10 @@ const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</div> </div>
</div> {variantIndex !== data.variants.length - 1 && (
<Hr className={classes.hr} />
)}
</React.Fragment>
); );
})} })}
</div> </div>

View file

@ -11,10 +11,10 @@ interface CreateVariantAttributeValueInput {
} }
type CreateVariantInput = CreateVariantAttributeValueInput[]; type CreateVariantInput = CreateVariantAttributeValueInput[];
function getAttributeValuePriceOrStock( function getAttributeValuePriceOrStock<T>(
attributes: CreateVariantInput, attributes: CreateVariantInput,
priceOrStock: AllOrAttribute priceOrStock: AllOrAttribute<T>
): string { ): T {
const attribute = attributes.find( const attribute = attributes.find(
attribute => attribute.attributeId === priceOrStock.attribute attribute => attribute.attributeId === priceOrStock.attribute
); );
@ -33,12 +33,9 @@ function createVariant(
const priceOverride = data.price.all const priceOverride = data.price.all
? data.price.value ? data.price.value
: getAttributeValuePriceOrStock(attributes, data.price); : getAttributeValuePriceOrStock(attributes, data.price);
const quantity = parseInt( const stocks = data.stock.all
data.stock.all ? data.stock.value
? data.stock.value : getAttributeValuePriceOrStock(attributes, data.stock);
: getAttributeValuePriceOrStock(attributes, data.stock),
10
);
return { return {
attributes: attributes.map(attribute => ({ attributes: attributes.map(attribute => ({
@ -46,8 +43,11 @@ function createVariant(
values: [attribute.attributeValueSlug] values: [attribute.attributeValueSlug]
})), })),
priceOverride, priceOverride,
quantity, sku: "",
sku: "" stocks: stocks.map((quantity, stockIndex) => ({
quantity: parseInt(quantity, 10),
warehouse: data.warehouses[stockIndex]
}))
}; };
} }

View file

@ -1,15 +1,16 @@
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
export interface AttributeValue { export interface AttributeValue<T> {
slug: string; slug: string;
value: string; value: T;
} }
export interface AllOrAttribute { export interface AllOrAttribute<T> {
all: boolean; all: boolean;
attribute: string; attribute: string;
value: string; value: T;
values: AttributeValue[]; values: Array<AttributeValue<T>>;
} }
export interface Attribute { export interface Attribute {
id: string; id: string;
@ -17,14 +18,16 @@ export interface Attribute {
} }
export interface ProductVariantCreateFormData { export interface ProductVariantCreateFormData {
attributes: Attribute[]; attributes: Attribute[];
price: AllOrAttribute; price: AllOrAttribute<string>;
stock: AllOrAttribute; stock: AllOrAttribute<number[]>;
variants: ProductVariantBulkCreateInput[]; variants: ProductVariantBulkCreateInput[];
warehouses: string[];
} }
export const createInitialForm = ( export const createInitialForm = (
attributes: ProductDetails_product_productType_variantAttributes[], attributes: ProductDetails_product_productType_variantAttributes[],
price: string price: string,
warehouses: WarehouseFragment[]
): ProductVariantCreateFormData => ({ ): ProductVariantCreateFormData => ({
attributes: attributes.map(attribute => ({ attributes: attributes.map(attribute => ({
id: attribute.id, id: attribute.id,
@ -39,8 +42,9 @@ export const createInitialForm = (
stock: { stock: {
all: true, all: true,
attribute: undefined, attribute: undefined,
value: "", value: warehouses.length === 1 ? [0] : [],
values: [] values: []
}, },
variants: [] variants: [],
warehouses: warehouses.length === 1 ? [warehouses[0].id] : []
}); });

View file

@ -3,8 +3,10 @@ import {
remove, remove,
removeAtIndex, removeAtIndex,
toggle, toggle,
updateAtIndex updateAtIndex,
update
} from "@saleor/utils/lists"; } from "@saleor/utils/lists";
import { StockInput } from "@saleor/types/globalTypes";
import { createVariants } from "./createVariants"; import { createVariants } from "./createVariants";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
@ -20,20 +22,24 @@ export type ProductVariantCreateReducerActionType =
| "changeAttributeValuePrice" | "changeAttributeValuePrice"
| "changeAttributeValueStock" | "changeAttributeValueStock"
| "changeVariantData" | "changeVariantData"
| "changeVariantStockData"
| "deleteVariant" | "deleteVariant"
| "reload" | "reload"
| "selectValue"; | "selectValue";
export type VariantField = "stock" | "price" | "sku"; export type VariantField = "price" | "sku";
export interface ProductVariantCreateReducerAction { export interface ProductVariantCreateReducerAction {
all?: boolean; all?: boolean;
attributeId?: string; attributeId?: string;
data?: ProductVariantCreateFormData; data?: ProductVariantCreateFormData;
field?: VariantField; field?: VariantField;
quantity?: number;
stock?: StockInput;
type: ProductVariantCreateReducerActionType; type: ProductVariantCreateReducerActionType;
value?: string; value?: string;
valueId?: string; valueId?: string;
variantIndex?: number; variantIndex?: number;
warehouseIndex?: number;
} }
function selectValue( function selectValue(
@ -70,7 +76,7 @@ function selectValue(
? toggle( ? toggle(
{ {
slug: valueSlug, slug: valueSlug,
value: "" value: []
}, },
prevState.stock.values, prevState.stock.values,
(a, b) => a.slug === b.slug (a, b) => a.slug === b.slug
@ -95,36 +101,26 @@ function applyPriceToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean value: boolean
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const data = { return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
all: value all: value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function applyStockToAll( function applyStockToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean value: boolean
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const data = { return {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
all: value all: value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeAttributeValuePrice( function changeAttributeValuePrice(
@ -149,24 +145,20 @@ function changeAttributeValuePrice(
index index
); );
const data = { return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeAttributeValueStock( function changeAttributeValueStock(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attributeValueSlug: string, attributeValueSlug: string,
stock: string warehouseIndex: number,
quantity: number
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const index = state.stock.values.findIndex( const index = state.stock.values.findIndex(
value => value.slug === attributeValueSlug value => value.slug === attributeValueSlug
@ -179,24 +171,19 @@ function changeAttributeValueStock(
const values = updateAtIndex( const values = updateAtIndex(
{ {
slug: attributeValueSlug, slug: attributeValueSlug,
value: stock value: updateAtIndex(quantity, state.stock.value, warehouseIndex)
}, },
state.stock.values, state.stock.values,
index index
); );
const data = { return {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyPriceToAttributeId( function changeApplyPriceToAttributeId(
@ -210,7 +197,8 @@ function changeApplyPriceToAttributeId(
slug, slug,
value: "" value: ""
})); }));
const data = {
return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
@ -218,11 +206,6 @@ function changeApplyPriceToAttributeId(
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyStockToAttributeId( function changeApplyStockToAttributeId(
@ -234,10 +217,10 @@ function changeApplyStockToAttributeId(
); );
const values = attribute.values.map(slug => ({ const values = attribute.values.map(slug => ({
slug, slug,
value: "" value: []
})); }));
const data = { return {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
@ -245,47 +228,33 @@ function changeApplyStockToAttributeId(
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyPriceToAllValue( function changeApplyPriceToAllValue(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: string value: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const data = { return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
value value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyStockToAllValue( function changeApplyStockToAllValue(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: string warehouseIndex: number,
quantity: number
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const data = { return {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
value value: updateAtIndex(quantity, state.stock.value, warehouseIndex)
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeVariantData( function changeVariantData(
@ -299,8 +268,6 @@ function changeVariantData(
variant.priceOverride = value; variant.priceOverride = value;
} else if (field === "sku") { } else if (field === "sku") {
variant.sku = value; variant.sku = value;
} else {
variant.quantity = parseInt(value, 10);
} }
return { return {
@ -309,6 +276,24 @@ function changeVariantData(
}; };
} }
function changeVariantStockData(
state: ProductVariantCreateFormData,
stock: StockInput,
variantIndex: number
): ProductVariantCreateFormData {
const variant = state.variants[variantIndex];
variant.stocks = update(
stock,
variant.stocks,
(a, b) => a.warehouse === b.warehouse
);
return {
...state,
variants: updateAtIndex(variant, state.variants, variantIndex)
};
}
function deleteVariant( function deleteVariant(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
variantIndex: number variantIndex: number
@ -319,6 +304,15 @@ function deleteVariant(
}; };
} }
function createVariantMatrix(
state: ProductVariantCreateFormData
): ProductVariantCreateFormData {
return {
...state,
variants: createVariants(state)
};
}
function reduceProductVariantCreateFormData( function reduceProductVariantCreateFormData(
prevState: ProductVariantCreateFormData, prevState: ProductVariantCreateFormData,
action: ProductVariantCreateReducerAction action: ProductVariantCreateReducerAction
@ -326,7 +320,6 @@ function reduceProductVariantCreateFormData(
switch (action.type) { switch (action.type) {
case "selectValue": case "selectValue":
return selectValue(prevState, action.attributeId, action.valueId); return selectValue(prevState, action.attributeId, action.valueId);
case "applyPriceToAll": case "applyPriceToAll":
return applyPriceToAll(prevState, action.all); return applyPriceToAll(prevState, action.all);
case "applyStockToAll": case "applyStockToAll":
@ -334,7 +327,12 @@ function reduceProductVariantCreateFormData(
case "changeAttributeValuePrice": case "changeAttributeValuePrice":
return changeAttributeValuePrice(prevState, action.valueId, action.value); return changeAttributeValuePrice(prevState, action.valueId, action.value);
case "changeAttributeValueStock": case "changeAttributeValueStock":
return changeAttributeValueStock(prevState, action.valueId, action.value); return changeAttributeValueStock(
prevState,
action.valueId,
action.quantity,
action.warehouseIndex
);
case "changeApplyPriceToAttributeId": case "changeApplyPriceToAttributeId":
return changeApplyPriceToAttributeId(prevState, action.attributeId); return changeApplyPriceToAttributeId(prevState, action.attributeId);
case "changeApplyStockToAttributeId": case "changeApplyStockToAttributeId":
@ -342,7 +340,11 @@ function reduceProductVariantCreateFormData(
case "changeApplyPriceToAllValue": case "changeApplyPriceToAllValue":
return changeApplyPriceToAllValue(prevState, action.value); return changeApplyPriceToAllValue(prevState, action.value);
case "changeApplyStockToAllValue": case "changeApplyStockToAllValue":
return changeApplyStockToAllValue(prevState, action.value); return changeApplyStockToAllValue(
prevState,
action.quantity,
action.warehouseIndex
);
case "changeVariantData": case "changeVariantData":
return changeVariantData( return changeVariantData(
prevState, prevState,
@ -350,10 +352,16 @@ function reduceProductVariantCreateFormData(
action.value, action.value,
action.variantIndex action.variantIndex
); );
case "changeVariantStockData":
return changeVariantStockData(
prevState,
action.stock,
action.variantIndex
);
case "deleteVariant": case "deleteVariant":
return deleteVariant(prevState, action.variantIndex); return deleteVariant(prevState, action.variantIndex);
case "reload": case "reload":
return action.data; return action.data ? action.data : createVariantMatrix(prevState);
default: default:
return prevState; return prevState;
} }

View file

@ -1,6 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeQuery from "@saleor/hooks/makeQuery"; import makeQuery from "@saleor/hooks/makeQuery";
import { warehouseFragment } from "@saleor/warehouses/queries";
import { pageInfoFragment, TypedQuery } from "../queries"; import { pageInfoFragment, TypedQuery } from "../queries";
import { import {
AvailableInGridAttributes, AvailableInGridAttributes,
@ -480,6 +481,7 @@ export const AvailableInGridAttributesQuery = TypedQuery<
const createMultipleVariantsData = gql` const createMultipleVariantsData = gql`
${fragmentMoney} ${fragmentMoney}
${productVariantAttributesFragment} ${productVariantAttributesFragment}
${warehouseFragment}
query CreateMultipleVariantsData($id: ID!) { query CreateMultipleVariantsData($id: ID!) {
product(id: $id) { product(id: $id) {
...ProductVariantAttributesFragment ...ProductVariantAttributesFragment
@ -487,6 +489,13 @@ const createMultipleVariantsData = gql`
...Money ...Money
} }
} }
warehouses(first: 20) {
edges {
node {
...WarehouseFragment
}
}
}
} }
`; `;
export const useCreateMultipleVariantsData = makeQuery< export const useCreateMultipleVariantsData = makeQuery<

View file

@ -71,8 +71,25 @@ export interface CreateMultipleVariantsData_product {
basePrice: CreateMultipleVariantsData_product_basePrice | null; basePrice: CreateMultipleVariantsData_product_basePrice | null;
} }
export interface CreateMultipleVariantsData_warehouses_edges_node {
__typename: "Warehouse";
id: string;
name: string;
}
export interface CreateMultipleVariantsData_warehouses_edges {
__typename: "WarehouseCountableEdge";
node: CreateMultipleVariantsData_warehouses_edges_node;
}
export interface CreateMultipleVariantsData_warehouses {
__typename: "WarehouseCountableConnection";
edges: CreateMultipleVariantsData_warehouses_edges[];
}
export interface CreateMultipleVariantsData { export interface CreateMultipleVariantsData {
product: CreateMultipleVariantsData_product | null; product: CreateMultipleVariantsData_product | null;
warehouses: CreateMultipleVariantsData_warehouses | null;
} }
export interface CreateMultipleVariantsDataVariables { export interface CreateMultipleVariantsDataVariables {

View file

@ -63,6 +63,7 @@ const ProductVariantCreator: React.FC<ProductVariantCreatorProps> = ({
variables: { id, inputs } variables: { id, inputs }
}) })
} }
warehouses={data?.warehouses.edges.map(edge => edge.node)}
/> />
</> </>
); );

View file

@ -1223,7 +1223,7 @@ export interface ProductVariantBulkCreateInput {
costPrice?: any | null; costPrice?: any | null;
priceOverride?: any | null; priceOverride?: any | null;
sku: string; sku: string;
quantity?: number | null; stocks: StockInput[];
trackInventory?: boolean | null; trackInventory?: boolean | null;
weight?: any | null; weight?: any | null;
} }

View file

@ -10,7 +10,6 @@ import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit"; import EditIcon from "@material-ui/icons/Edit";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
@ -19,6 +18,7 @@ import { ListProps, SortPage } from "@saleor/types";
import { WarehouseListUrlSortField } from "@saleor/warehouses/urls"; import { WarehouseListUrlSortField } from "@saleor/warehouses/urls";
import TableCellHeader from "@saleor/components/TableCellHeader"; import TableCellHeader from "@saleor/components/TableCellHeader";
import { getArrowDirection } from "@saleor/utils/sort"; import { getArrowDirection } from "@saleor/utils/sort";
import { WarehouseWithShippingFragment } from "@saleor/warehouses/types/WarehouseWithShippingFragment";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
@ -59,7 +59,7 @@ const useStyles = makeStyles(
interface WarehouseListProps interface WarehouseListProps
extends ListProps, extends ListProps,
SortPage<WarehouseListUrlSortField> { SortPage<WarehouseListUrlSortField> {
warehouses: WarehouseFragment[]; warehouses: WarehouseWithShippingFragment[];
onAdd: () => void; onAdd: () => void;
onRemove: (id: string) => void; onRemove: (id: string) => void;
} }

View file

@ -3,7 +3,6 @@ import Card from "@material-ui/core/Card";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SearchBar from "@saleor/components/SearchBar"; import SearchBar from "@saleor/components/SearchBar";
@ -16,6 +15,7 @@ import {
} from "@saleor/types"; } from "@saleor/types";
import { WarehouseListUrlSortField } from "@saleor/warehouses/urls"; import { WarehouseListUrlSortField } from "@saleor/warehouses/urls";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { WarehouseWithShippingFragment } from "@saleor/warehouses/types/WarehouseWithShippingFragment";
import WarehouseList from "../WarehouseList"; import WarehouseList from "../WarehouseList";
export interface WarehouseListPageProps export interface WarehouseListPageProps
@ -23,7 +23,7 @@ export interface WarehouseListPageProps
SearchPageProps, SearchPageProps,
SortPage<WarehouseListUrlSortField>, SortPage<WarehouseListUrlSortField>,
TabPageProps { TabPageProps {
warehouses: WarehouseFragment[]; warehouses: WarehouseWithShippingFragment[];
onBack: () => void; onBack: () => void;
onRemove: (id: string) => void; onRemove: (id: string) => void;
} }

View file

@ -13,6 +13,12 @@ export const warehouseFragment = gql`
fragment WarehouseFragment on Warehouse { fragment WarehouseFragment on Warehouse {
id id
name name
}
`;
export const warehouseWithShippingFragment = gql`
${warehouseFragment}
fragment WarehouseWithShippingFragment on Warehouse {
...WarehouseFragment
shippingZones(first: 100) { shippingZones(first: 100) {
edges { edges {
node { node {
@ -26,9 +32,9 @@ export const warehouseFragment = gql`
export const warehouseDetailsFragment = gql` export const warehouseDetailsFragment = gql`
${fragmentAddress} ${fragmentAddress}
${warehouseFragment} ${warehouseWithShippingFragment}
fragment WarehouseDetailsFragment on Warehouse { fragment WarehouseDetailsFragment on Warehouse {
...WarehouseFragment ...WarehouseWithShippingFragment
address { address {
...AddressFragment ...AddressFragment
} }
@ -36,7 +42,7 @@ export const warehouseDetailsFragment = gql`
`; `;
const warehouseList = gql` const warehouseList = gql`
${warehouseFragment} ${warehouseWithShippingFragment}
${pageInfoFragment} ${pageInfoFragment}
query WarehouseList( query WarehouseList(
$first: Int $first: Int
@ -56,7 +62,7 @@ const warehouseList = gql`
) { ) {
edges { edges {
node { node {
...WarehouseFragment ...WarehouseWithShippingFragment
} }
} }
pageInfo { pageInfo {

View file

@ -6,25 +6,8 @@
// GraphQL fragment: WarehouseFragment // GraphQL fragment: WarehouseFragment
// ==================================================== // ====================================================
export interface WarehouseFragment_shippingZones_edges_node {
__typename: "ShippingZone";
id: string;
name: string;
}
export interface WarehouseFragment_shippingZones_edges {
__typename: "ShippingZoneCountableEdge";
node: WarehouseFragment_shippingZones_edges_node;
}
export interface WarehouseFragment_shippingZones {
__typename: "ShippingZoneCountableConnection";
edges: WarehouseFragment_shippingZones_edges[];
}
export interface WarehouseFragment { export interface WarehouseFragment {
__typename: "Warehouse"; __typename: "Warehouse";
id: string; id: string;
name: string; name: string;
shippingZones: WarehouseFragment_shippingZones;
} }

View file

@ -0,0 +1,30 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: WarehouseWithShippingFragment
// ====================================================
export interface WarehouseWithShippingFragment_shippingZones_edges_node {
__typename: "ShippingZone";
id: string;
name: string;
}
export interface WarehouseWithShippingFragment_shippingZones_edges {
__typename: "ShippingZoneCountableEdge";
node: WarehouseWithShippingFragment_shippingZones_edges_node;
}
export interface WarehouseWithShippingFragment_shippingZones {
__typename: "ShippingZoneCountableConnection";
edges: WarehouseWithShippingFragment_shippingZones_edges[];
}
export interface WarehouseWithShippingFragment {
__typename: "Warehouse";
id: string;
name: string;
shippingZones: WarehouseWithShippingFragment_shippingZones;
}