Merge pull request #515 from mirumee/ref/add-warehouse-stocck

Update product stock management to newest design
This commit is contained in:
Dominik Żegleń 2020-05-05 19:25:11 +02:00 committed by GitHub
commit 02b3717121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1093 additions and 920 deletions

View file

@ -4,6 +4,8 @@ All notable, unreleased changes to this project will be documented in this file.
## [Unreleased]
- Update product stock management to newest design - #515 by @dominik-zeglen
## 2.10.0
- Fix minor bugs - #244 by @dominik-zeglen

View file

@ -1427,6 +1427,9 @@
"src_dot_configuration_dot_1233229030": {
"string": "Miscellaneous"
},
"src_dot_configuration_dot_1440737903": {
"string": "Shipping Settings"
},
"src_dot_configuration_dot_1639245766": {
"string": "View and update your webhook and their settings"
},
@ -3074,6 +3077,10 @@
"context": "dialog title",
"string": "Delete permission group"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDeleteDialog_dot_956177443": {
"context": "deletion error message",
"string": "Cant's delete group which is out of your permission scope"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDetailsPage_dot_3765873075": {
"context": "checkbox label",
"string": "Group has full access to the store"

View file

@ -172,7 +172,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
disabled={loading}
errors={updateResult.data?.categoryUpdate.errors || []}
onAddCategory={() => navigate(categoryAddUrl(id))}
onAddProduct={() => navigate(productAddUrl())}
onAddProduct={() => navigate(productAddUrl)}
onBack={() =>
navigate(
maybe(

View file

@ -46,7 +46,7 @@ export function searchInCommands(
{
label: intl.formatMessage(messages.createProduct),
onClick: () => {
navigate(productAddUrl());
navigate(productAddUrl);
return false;
}
},

View file

@ -1,3 +1,4 @@
import { removeAtIndex } from "@saleor/utils/lists";
import useStateFromProps from "./useStateFromProps";
export type FormsetChange<TValue = any> = (id: string, value: TValue) => void;
@ -11,11 +12,13 @@ export type FormsetData<TData = object, TValue = any> = Array<
FormsetAtomicData<TData, TValue>
>;
export interface UseFormsetOutput<TData = object, TValue = any> {
add: (data: FormsetAtomicData<TData, TValue>) => void;
change: FormsetChange<TValue>;
data: FormsetData<TData, TValue>;
get: (id: string) => FormsetAtomicData<TData, TValue>;
// Used for some rare situations like dataset change
set: (data: FormsetData<TData, TValue>) => void;
remove: (id: string) => void;
}
function useFormset<TData = object, TValue = any>(
initial: FormsetData<TData, TValue>
@ -24,10 +27,23 @@ function useFormset<TData = object, TValue = any>(
initial || []
);
function addItem(itemData: FormsetAtomicData<TData, TValue>) {
setData(prevData => [...prevData, itemData]);
}
function getItem(id: string): FormsetAtomicData<TData, TValue> {
return data.find(item => item.id === id);
}
function removeItem(id: string) {
setData(prevData =>
removeAtIndex(
prevData,
prevData.findIndex(item => item.id === id)
)
);
}
function setItemValue(id: string, value: TValue) {
const itemIndex = data.findIndex(item => item.id === id);
setData([
@ -41,9 +57,11 @@ function useFormset<TData = object, TValue = any>(
}
return {
add: addItem,
change: setItemValue,
data,
get: getItem,
remove: removeItem,
set: setData
};
}

View file

@ -338,7 +338,10 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
warehouseStock.quantityAllocated;
return (
<TableCell className={classes.colQuantity}>
<TableCell
className={classes.colQuantity}
key={warehouseStock.id}
>
<div className={classes.colQuantityContent}>
<TextField
type="number"

View file

@ -86,7 +86,6 @@ interface ProductCreatePageProps {
fetchCategories: (data: string) => void;
fetchCollections: (data: string) => void;
fetchProductTypes: (data: string) => void;
onWarehouseEdit: () => void;
onBack?();
onSubmit?(data: ProductCreatePageSubmitData);
}
@ -108,8 +107,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
warehouses,
onBack,
fetchProductTypes,
onSubmit,
onWarehouseEdit
onSubmit
}: ProductCreatePageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
@ -119,18 +117,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
data: attributes,
set: setAttributeData
} = useFormset<ProductAttributeInputData>([]);
const { change: changeStockData, data: stocks, set: setStocks } = useFormset<
null
>([]);
React.useEffect(() => {
const newStocks = warehouses.map(warehouse => ({
data: null,
id: warehouse.id,
label: warehouse.name,
value: stocks.find(stock => stock.id === warehouse.id)?.value || 0
}));
setStocks(newStocks);
}, [JSON.stringify(warehouses)]);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset<null, string>([]);
// Ensures that it will not change after component rerenders, because it
// generates different block keys and it causes editor to lose its content.
@ -253,11 +245,29 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
<ProductStocks
data={data}
disabled={disabled}
onChange={changeStockData}
onFormDataChange={change}
errors={errors}
stocks={stocks}
onWarehousesEdit={onWarehouseEdit}
warehouses={warehouses}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
/>
<CardSpacer />
</>

View file

@ -1,4 +1,8 @@
import Button from "@material-ui/core/Button";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow";
import Popper from "@material-ui/core/Popper";
import { fade } from "@material-ui/core/styles/colorManipulator";
import IconButton from "@material-ui/core/IconButton";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Table from "@material-ui/core/Table";
@ -11,6 +15,10 @@ import Typography from "@material-ui/core/Typography";
import React from "react";
import { useIntl, FormattedMessage } from "react-intl";
import makeStyles from "@material-ui/core/styles/makeStyles";
import AddIcon from "@material-ui/icons/Add";
import DeleteIcon from "@material-ui/icons/Delete";
import Paper from "@material-ui/core/Paper";
import MenuItem from "@material-ui/core/MenuItem";
import { FormChange } from "@saleor/hooks/useForm";
import { FormsetChange, FormsetAtomicData } from "@saleor/hooks/useFormset";
@ -21,7 +29,8 @@ import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { renderCollection } from "@saleor/misc";
import Link from "@saleor/components/Link";
import { ICONBUTTON_SIZE } from "@saleor/theme";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
export type ProductStockInput = FormsetAtomicData<null, string>;
export interface ProductStockFormData {
@ -34,13 +43,19 @@ export interface ProductStocksProps {
disabled: boolean;
errors: UserError[];
stocks: ProductStockInput[];
warehouses: WarehouseFragment[];
onChange: FormsetChange;
onFormDataChange: FormChange;
onWarehousesEdit: () => void;
onWarehouseStockAdd: (warehouseId: string) => void;
onWarehouseStockDelete: (warehouseId: string) => void;
}
const useStyles = makeStyles(
theme => ({
colAction: {
padding: 0,
width: ICONBUTTON_SIZE + theme.spacing()
},
colName: {},
colQuantity: {
textAlign: "right",
@ -56,6 +71,19 @@ const useStyles = makeStyles(
inputComponent: {
width: 100
},
menuItem: {
"&:not(:last-of-type)": {
marginBottom: theme.spacing(2)
}
},
paper: {
padding: theme.spacing(2)
},
popper: {
boxShadow: `0px 5px 10px 0 ${fade(theme.palette.common.black, 0.05)}`,
marginTop: theme.spacing(1),
zIndex: 2
},
quantityContainer: {
paddingTop: theme.spacing()
},
@ -80,12 +108,20 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
disabled,
errors,
stocks,
warehouses,
onChange,
onFormDataChange,
onWarehousesEdit
onWarehouseStockAdd,
onWarehouseStockDelete
}) => {
const classes = useStyles({});
const intl = useIntl();
const anchor = React.useRef<HTMLDivElement>();
const [isExpanded, setExpansionState] = React.useState(false);
const warehousesToAssign = warehouses.filter(
warehouse => !stocks.some(stock => stock.id === warehouse.id)
);
return (
<Card>
@ -140,17 +176,6 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
description="header"
/>
</span>
<Button
className={classes.editWarehouses}
color="primary"
data-cy="edit-warehouses"
onClick={onWarehousesEdit}
>
<FormattedMessage
defaultMessage="Edit Warehouses"
description="button"
/>
</Button>
</div>
</Typography>
</CardContent>
@ -169,44 +194,89 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
description="tabel column header"
/>
</TableCell>
<TableCell className={classes.colAction} />
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
stocks,
stock => (
<TableRow key={stock.id}>
<TableCell className={classes.colName}>{stock.label}</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
className={classes.inputComponent}
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number"
}}
onChange={event => onChange(stock.id, event.target.value)}
value={stock.value}
/>
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={2}>
{renderCollection(stocks, stock => (
<TableRow key={stock.id}>
<TableCell className={classes.colName}>{stock.label}</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
className={classes.inputComponent}
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number"
}}
onChange={event => onChange(stock.id, event.target.value)}
value={stock.value}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
color="primary"
onClick={() => onWarehouseStockDelete(stock.id)}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{warehousesToAssign.length > 0 && (
<TableRow>
<TableCell colSpan={2}>
<Typography variant="body2">
<FormattedMessage
defaultMessage={
"This product doesn't have any stock. You can add it <l>here</l>."
}
values={{
l: str => <Link onClick={onWarehousesEdit}>{str}</Link>
}}
defaultMessage="Assign Warehouse"
description="button"
/>
</TableCell>
</TableRow>
)
</Typography>
</TableCell>
<TableCell className={classes.colAction}>
<ClickAwayListener onClickAway={() => setExpansionState(false)}>
<div ref={anchor}>
<IconButton
color="primary"
onClick={() => setExpansionState(!isExpanded)}
>
<AddIcon />
</IconButton>
<Popper
className={classes.popper}
open={isExpanded}
anchorEl={anchor.current}
transition
placement="top-end"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: "right top"
}}
>
<Paper className={classes.paper}>
{warehousesToAssign.map(warehouse => (
<MenuItem
className={classes.menuItem}
onClick={() =>
onWarehouseStockAdd(warehouse.id)
}
>
{warehouse.name}
</MenuItem>
))}
</Paper>
</Grow>
)}
</Popper>
</div>
</ClickAwayListener>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>

View file

@ -1,4 +1,5 @@
import { convertFromRaw, RawDraftContentState } from "draft-js";
import { diff } from "fast-array-diff";
import React from "react";
import { useIntl } from "react-intl";
@ -23,6 +24,7 @@ import { FetchMoreProps, ListActions } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import {
ProductDetails_product,
ProductDetails_product_images,
@ -62,9 +64,9 @@ export interface ProductUpdatePageProps extends ListActions {
product: ProductDetails_product;
header: string;
saveButtonBarState: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[];
fetchCategories: (query: string) => void;
fetchCollections: (query: string) => void;
onWarehousesEdit: () => void;
onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void;
onImageDelete: (id: string) => () => void;
@ -81,7 +83,9 @@ export interface ProductUpdatePageProps extends ListActions {
export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData {
attributes: ProductAttributeInput[];
collections: string[];
stocks: ProductStockInput[];
addStocks: ProductStockInput[];
updateStocks: ProductStockInput[];
removeStocks: string[];
}
export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
@ -99,6 +103,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
product,
saveButtonBarState,
variants,
warehouses,
onBack,
onDelete,
onImageDelete,
@ -110,7 +115,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onVariantAdd,
onVariantsAdd,
onVariantShow,
onWarehousesEdit,
isChecked,
selected,
toggle,
@ -129,7 +133,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks } = useFormset(stockInput);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset(stockInput);
const [selectedAttributes, setSelectedAttributes] = useStateFromProps<
ProductAttributeValueChoices[]
@ -153,12 +162,25 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const currency = maybe(() => product.basePrice.currency);
const hasVariants = maybe(() => product.productType.hasVariants, false);
const handleSubmit = (data: ProductUpdatePageFormData) =>
const handleSubmit = (data: ProductUpdatePageFormData) => {
const dataStocks = stocks.map(stock => stock.id);
const variantStocks = product.variants[0].stocks.map(
stock => stock.warehouse.id
);
const stockDiff = diff(variantStocks, dataStocks);
onSubmit({
...data,
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
attributes,
stocks,
...data
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
});
};
return (
<Form onSubmit={handleSubmit} initial={initialData} confirmLeave>
@ -252,12 +274,27 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
disabled={disabled}
errors={errors}
stocks={stocks}
warehouses={warehouses}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehousesEdit={onWarehousesEdit}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
/>
)}
<CardSpacer />

View file

@ -51,7 +51,6 @@ interface ProductVariantCreatePageProps {
onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void;
onVariantClick: (variantId: string) => void;
onWarehouseEdit: () => void;
}
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
@ -64,8 +63,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
warehouses,
onBack,
onSubmit,
onVariantClick,
onWarehouseEdit
onVariantClick
}) => {
const intl = useIntl();
const attributeInput = React.useMemo(
@ -75,18 +73,12 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks, set: setStocks } = useFormset<
null
>([]);
React.useEffect(() => {
const newStocks = warehouses.map(warehouse => ({
data: null,
id: warehouse.id,
label: warehouse.name,
value: stocks.find(stock => stock.id === warehouse.id)?.value || 0
}));
setStocks(newStocks);
}, [JSON.stringify(warehouses)]);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset<null, string>([]);
const initialForm: ProductVariantCreatePageFormData = {
costPrice: "",
@ -148,11 +140,28 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
<ProductStocks
data={data}
disabled={disabled}
onChange={changeStockData}
onFormDataChange={change}
errors={errors}
stocks={stocks}
onWarehousesEdit={onWarehouseEdit}
warehouses={warehouses}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(warehouse => warehouse.id === id)
.name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
/>
</div>
</Grid>

View file

@ -1,4 +1,5 @@
import React from "react";
import { diff } from "fast-array-diff";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
@ -17,6 +18,7 @@ import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import { maybe } from "../../../misc";
import { ProductVariant } from "../../types/ProductVariant";
import ProductVariantAttributes, {
@ -38,7 +40,9 @@ export interface ProductVariantPageFormData {
export interface ProductVariantPageSubmitData
extends ProductVariantPageFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
stocks: ProductStockInput[];
addStocks: ProductStockInput[];
updateStocks: ProductStockInput[];
removeStocks: string[];
}
interface ProductVariantPageProps {
@ -48,7 +52,7 @@ interface ProductVariantPageProps {
loading?: boolean;
placeholderImage?: string;
header: string;
onWarehousesEdit: () => void;
warehouses: WarehouseFragment[];
onAdd();
onBack();
onDelete();
@ -64,12 +68,12 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
placeholderImage,
saveButtonBarState,
variant,
warehouses,
onAdd,
onBack,
onDelete,
onImageSelect,
onSubmit,
onWarehousesEdit,
onVariantClick
}) => {
const attributeInput = React.useMemo(
@ -82,7 +86,12 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks } = useFormset(stockInput);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset(stockInput);
const [isModalOpened, setModalStatus] = React.useState(false);
const toggleModal = () => setModalStatus(!isModalOpened);
@ -106,12 +115,23 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
trackInventory: variant?.trackInventory
};
const handleSubmit = (data: ProductVariantPageFormData) =>
const handleSubmit = (data: ProductVariantPageFormData) => {
const dataStocks = stocks.map(stock => stock.id);
const variantStocks = variant.stocks.map(stock => stock.warehouse.id);
const stockDiff = diff(variantStocks, dataStocks);
onSubmit({
...data,
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
attributes,
stocks
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
});
};
return (
<>
@ -180,12 +200,27 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
disabled={loading}
errors={errors}
stocks={stocks}
warehouses={warehouses}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehousesEdit={onWarehousesEdit}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
/>
</div>
</Grid>

View file

@ -1,46 +0,0 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "@saleor/storybook//Decorator";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { StockErrorCode } from "@saleor/types/globalTypes";
import ProductWarehousesDialog, {
ProductWarehousesDialogProps
} from "./ProductWarehousesDialog";
const props: ProductWarehousesDialogProps = {
confirmButtonState: "default",
disabled: false,
errors: [],
onClose: () => undefined,
onConfirm: () => undefined,
open: true,
warehouses: warehouseList,
warehousesWithStocks: [warehouseList[0].id, warehouseList[2].id]
};
storiesOf("Views / Products / Edit warehouses", module)
.addDecorator(Decorator)
.add("default", () => <ProductWarehousesDialog {...props} />)
.add("loading warehouses", () => (
<ProductWarehousesDialog {...props} warehouses={undefined} />
))
.add("loading confirmation", () => (
<ProductWarehousesDialog
{...props}
confirmButtonState="loading"
disabled={true}
/>
))
.add("with error", () => (
<ProductWarehousesDialog
{...props}
errors={[
{
__typename: "StockError",
code: StockErrorCode.INVALID,
field: null
}
]}
/>
));

View file

@ -1,152 +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 Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
import makeStyles from "@material-ui/core/styles/makeStyles";
import { diff, DiffData } from "fast-array-diff";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import { buttonMessages } from "@saleor/intl";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import Skeleton from "@saleor/components/Skeleton";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import { isSelected, toggle } from "@saleor/utils/lists";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { BulkStockErrorFragment } from "@saleor/products/types/BulkStockErrorFragment";
import { StockErrorFragment } from "@saleor/products/types/StockErrorFragment";
import getStockErrorMessage, {
getBulkStockErrorMessage
} from "@saleor/utils/errors/stock";
const useStyles = makeStyles(
theme => ({
dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.palette.divider}`
},
errorParagraph: {
paddingTop: 0
},
helperText: {
marginBottom: theme.spacing(1)
}
}),
{
name: "ProductWarehousesDialog"
}
);
export interface ProductWarehousesDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
errors: Array<BulkStockErrorFragment | StockErrorFragment>;
open: boolean;
warehouses: SearchWarehouses_search_edges_node[];
warehousesWithStocks: string[];
onClose: () => void;
onConfirm: (data: DiffData<string>) => void;
}
function getErrorMessage(
err: BulkStockErrorFragment | StockErrorFragment,
intl: IntlShape
): string {
switch (err?.__typename) {
case "BulkStockError":
return getBulkStockErrorMessage(err, intl);
default:
return getStockErrorMessage(err, intl);
}
}
const ProductWarehousesDialog: React.FC<ProductWarehousesDialogProps> = ({
confirmButtonState,
disabled,
errors,
onClose,
onConfirm,
open,
warehousesWithStocks,
warehouses
}) => {
const classes = useStyles({});
const intl = useIntl();
const [selectedWarehouses, setSelectedWarehouses] = useStateFromProps(
warehousesWithStocks || []
);
const handleConfirm = () =>
onConfirm(diff(warehousesWithStocks, selectedWarehouses));
return (
<Dialog onClose={onClose} maxWidth="sm" fullWidth open={open}>
<DialogTitle>
<FormattedMessage
defaultMessage="Edit Warehouses"
description="dialog header"
/>
</DialogTitle>
<form>
<DialogContent>
<Typography className={classes.helperText}>
<FormattedMessage defaultMessage="Select warehouses that stock selected product" />
</Typography>
{warehouses === undefined ? (
<Skeleton />
) : (
warehouses.map(warehouse => (
<div key={warehouse.id}>
<ControlledCheckbox
checked={isSelected(
warehouse.id,
selectedWarehouses,
(a, b) => a === b
)}
name={`warehouse:${warehouse.id}`}
onChange={() =>
setSelectedWarehouses(
toggle(
warehouse.id,
selectedWarehouses,
(a, b) => a === b
)
)
}
disabled={disabled}
label={warehouse.name}
/>
</div>
))
)}
</DialogContent>
{errors.length > 0 && (
<DialogContent className={classes.errorParagraph}>
<Typography color="error">
{getErrorMessage(errors[0], intl)}
</Typography>
</DialogContent>
)}
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
onClick={handleConfirm}
>
<FormattedMessage {...buttonMessages.save} />
</ConfirmButton>
</DialogActions>
</form>
</Dialog>
);
};
ProductWarehousesDialog.displayName = "ProductWarehousesDialog";
export default ProductWarehousesDialog;

View file

@ -1,2 +0,0 @@
export { default } from "./ProductWarehousesDialog";
export * from "./ProductWarehousesDialog";

View file

@ -19,11 +19,9 @@ import {
productVariantAddPath,
productVariantEditPath,
ProductVariantEditUrlQueryParams,
ProductAddUrlQueryParams,
ProductVariantAddUrlQueryParams,
productVariantCreatorPath
} from "./urls";
import ProductCreateComponent from "./views/ProductCreate";
import ProductCreate from "./views/ProductCreate";
import ProductImageComponent from "./views/ProductImage";
import ProductListComponent from "./views/ProductList";
import ProductUpdateComponent from "./views/ProductUpdate";
@ -90,17 +88,11 @@ const ProductImage: React.FC<RouteComponentProps<any>> = ({
const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: ProductVariantAddUrlQueryParams = qs;
return (
<ProductVariantCreateComponent
productId={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
}) => (
<ProductVariantCreateComponent
productId={decodeURIComponent(match.params.id)}
/>
);
const ProductVariantCreator: React.FC<RouteComponentProps<{
id: string;
@ -108,13 +100,6 @@ const ProductVariantCreator: React.FC<RouteComponentProps<{
<ProductVariantCreatorComponent id={decodeURIComponent(match.params.id)} />
);
const ProductCreate: React.FC<RouteComponentProps> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: ProductAddUrlQueryParams = qs;
return <ProductCreateComponent params={params} />;
};
const Component = () => {
const intl = useIntl();

View file

@ -38,11 +38,7 @@ import {
} from "./types/VariantImageUnassign";
import { VariantUpdate, VariantUpdateVariables } from "./types/VariantUpdate";
import {
fragmentVariant,
productFragmentDetails,
stockFragment
} from "./queries";
import { fragmentVariant, productFragmentDetails } from "./queries";
import {
productBulkDelete,
productBulkDeleteVariables
@ -59,10 +55,6 @@ import {
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
} from "./types/ProductVariantBulkDelete";
import {
AddOrRemoveStocks,
AddOrRemoveStocksVariables
} from "./types/AddOrRemoveStocks";
export const bulkProductErrorFragment = gql`
fragment BulkProductErrorFragment on BulkProductError {
@ -359,6 +351,8 @@ export const variantUpdateMutation = gql`
${fragmentVariant}
${productErrorFragment}
mutation VariantUpdate(
$addStocks: [StockInput!]!
$removeStocks: [ID!]!
$id: ID!
$attributes: [AttributeValueInput]
$costPrice: Decimal
@ -392,6 +386,29 @@ export const variantUpdateMutation = gql`
...ProductVariant
}
}
productVariantStocksCreate(stocks: $addStocks, variantId: $id) {
errors: bulkStockErrors {
...BulkStockErrorFragment
}
productVariant {
id
stocks {
...StockFragment
}
}
}
productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) {
errors: stockErrors {
code
field
}
productVariant {
id
stocks {
...StockFragment
}
}
}
}
`;
export const TypedVariantUpdateMutation = TypedMutation<
@ -558,41 +575,3 @@ export const TypedProductVariantBulkDeleteMutation = TypedMutation<
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
>(ProductVariantBulkDeleteMutation);
const addOrRemoveStocks = gql`
${bulkStockErrorFragment}
${stockFragment}
mutation AddOrRemoveStocks(
$variantId: ID!
$add: [StockInput!]!
$remove: [ID!]!
) {
productVariantStocksCreate(stocks: $add, variantId: $variantId) {
errors: bulkStockErrors {
...BulkStockErrorFragment
}
productVariant {
id
stocks {
...StockFragment
}
}
}
productVariantStocksDelete(warehouseIds: $remove, variantId: $variantId) {
errors: stockErrors {
code
field
}
productVariant {
id
stocks {
...StockFragment
}
}
}
}
`;
export const useAddOrRemoveStocks = makeMutation<
AddOrRemoveStocks,
AddOrRemoveStocksVariables
>(addOrRemoveStocks);

View file

@ -1,85 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { StockInput, ProductErrorCode, StockErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AddOrRemoveStocks
// ====================================================
export interface AddOrRemoveStocks_productVariantStocksCreate_errors {
__typename: "BulkStockError";
code: ProductErrorCode;
field: string | null;
index: number | null;
}
export interface AddOrRemoveStocks_productVariantStocksCreate_productVariant_stocks_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface AddOrRemoveStocks_productVariantStocksCreate_productVariant_stocks {
__typename: "Stock";
id: string;
quantity: number;
quantityAllocated: number;
warehouse: AddOrRemoveStocks_productVariantStocksCreate_productVariant_stocks_warehouse;
}
export interface AddOrRemoveStocks_productVariantStocksCreate_productVariant {
__typename: "ProductVariant";
id: string;
stocks: (AddOrRemoveStocks_productVariantStocksCreate_productVariant_stocks | null)[] | null;
}
export interface AddOrRemoveStocks_productVariantStocksCreate {
__typename: "ProductVariantStocksCreate";
errors: AddOrRemoveStocks_productVariantStocksCreate_errors[];
productVariant: AddOrRemoveStocks_productVariantStocksCreate_productVariant | null;
}
export interface AddOrRemoveStocks_productVariantStocksDelete_errors {
__typename: "StockError";
code: StockErrorCode;
field: string | null;
}
export interface AddOrRemoveStocks_productVariantStocksDelete_productVariant_stocks_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface AddOrRemoveStocks_productVariantStocksDelete_productVariant_stocks {
__typename: "Stock";
id: string;
quantity: number;
quantityAllocated: number;
warehouse: AddOrRemoveStocks_productVariantStocksDelete_productVariant_stocks_warehouse;
}
export interface AddOrRemoveStocks_productVariantStocksDelete_productVariant {
__typename: "ProductVariant";
id: string;
stocks: (AddOrRemoveStocks_productVariantStocksDelete_productVariant_stocks | null)[] | null;
}
export interface AddOrRemoveStocks_productVariantStocksDelete {
__typename: "ProductVariantStocksDelete";
errors: AddOrRemoveStocks_productVariantStocksDelete_errors[];
productVariant: AddOrRemoveStocks_productVariantStocksDelete_productVariant | null;
}
export interface AddOrRemoveStocks {
productVariantStocksCreate: AddOrRemoveStocks_productVariantStocksCreate | null;
productVariantStocksDelete: AddOrRemoveStocks_productVariantStocksDelete | null;
}
export interface AddOrRemoveStocksVariables {
variantId: string;
add: StockInput[];
remove: string[];
}

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueInput, StockInput, ProductErrorCode } from "./../../types/globalTypes";
import { StockInput, AttributeValueInput, ProductErrorCode, StockErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: VariantUpdate
@ -255,12 +255,81 @@ export interface VariantUpdate_productVariantStocksUpdate {
productVariant: VariantUpdate_productVariantStocksUpdate_productVariant | null;
}
export interface VariantUpdate_productVariantStocksCreate_errors {
__typename: "BulkStockError";
code: ProductErrorCode;
field: string | null;
index: number | null;
}
export interface VariantUpdate_productVariantStocksCreate_productVariant_stocks_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface VariantUpdate_productVariantStocksCreate_productVariant_stocks {
__typename: "Stock";
id: string;
quantity: number;
quantityAllocated: number;
warehouse: VariantUpdate_productVariantStocksCreate_productVariant_stocks_warehouse;
}
export interface VariantUpdate_productVariantStocksCreate_productVariant {
__typename: "ProductVariant";
id: string;
stocks: (VariantUpdate_productVariantStocksCreate_productVariant_stocks | null)[] | null;
}
export interface VariantUpdate_productVariantStocksCreate {
__typename: "ProductVariantStocksCreate";
errors: VariantUpdate_productVariantStocksCreate_errors[];
productVariant: VariantUpdate_productVariantStocksCreate_productVariant | null;
}
export interface VariantUpdate_productVariantStocksDelete_errors {
__typename: "StockError";
code: StockErrorCode;
field: string | null;
}
export interface VariantUpdate_productVariantStocksDelete_productVariant_stocks_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface VariantUpdate_productVariantStocksDelete_productVariant_stocks {
__typename: "Stock";
id: string;
quantity: number;
quantityAllocated: number;
warehouse: VariantUpdate_productVariantStocksDelete_productVariant_stocks_warehouse;
}
export interface VariantUpdate_productVariantStocksDelete_productVariant {
__typename: "ProductVariant";
id: string;
stocks: (VariantUpdate_productVariantStocksDelete_productVariant_stocks | null)[] | null;
}
export interface VariantUpdate_productVariantStocksDelete {
__typename: "ProductVariantStocksDelete";
errors: VariantUpdate_productVariantStocksDelete_errors[];
productVariant: VariantUpdate_productVariantStocksDelete_productVariant | null;
}
export interface VariantUpdate {
productVariantUpdate: VariantUpdate_productVariantUpdate | null;
productVariantStocksUpdate: VariantUpdate_productVariantStocksUpdate | null;
productVariantStocksCreate: VariantUpdate_productVariantStocksCreate | null;
productVariantStocksDelete: VariantUpdate_productVariantStocksDelete | null;
}
export interface VariantUpdateVariables {
addStocks: StockInput[];
removeStocks: string[];
id: string;
attributes?: (AttributeValueInput | null)[] | null;
costPrice?: any | null;

View file

@ -17,10 +17,7 @@ import {
const productSection = "/products/";
export const productAddPath = urlJoin(productSection, "add");
export type ProductAddUrlDialog = "edit-stocks";
export type ProductAddUrlQueryParams = Dialog<ProductAddUrlDialog>;
export const productAddUrl = (params?: ProductAddUrlQueryParams): string =>
productAddPath + "?" + stringifyQs(params);
export const productAddUrl = productAddPath;
export const productListPath = productSection;
export type ProductListUrlDialog =
@ -69,14 +66,14 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
productListPath + "?" + stringifyQs(params);
export const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog = "edit-stocks" | "remove" | "remove-variants";
export type ProductUrlDialog = "remove" | "remove-variants";
export type ProductUrlQueryParams = BulkAction & Dialog<ProductUrlDialog>;
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const productVariantEditPath = (productId: string, variantId: string) =>
urlJoin(productSection, productId, "variant", variantId);
export type ProductVariantEditUrlDialog = "edit-stocks" | "remove";
export type ProductVariantEditUrlDialog = "remove";
export type ProductVariantEditUrlQueryParams = Dialog<
ProductVariantEditUrlDialog
>;
@ -99,17 +96,8 @@ export const productVariantCreatorUrl = (productId: string) =>
export const productVariantAddPath = (productId: string) =>
urlJoin(productSection, productId, "variant/add");
export type ProductVariantAddUrlDialog = "edit-stocks";
export type ProductVariantAddUrlQueryParams = Dialog<
ProductVariantAddUrlDialog
>;
export const productVariantAddUrl = (
productId: string,
params?: ProductVariantAddUrlQueryParams
): string =>
productVariantAddPath(encodeURIComponent(productId)) +
"?" +
stringifyQs(params);
export const productVariantAddUrl = (productId: string): string =>
productVariantAddPath(encodeURIComponent(productId));
export const productImagePath = (productId: string, imageId: string) =>
urlJoin(productSection, productId, "image", imageId);

View file

@ -9,6 +9,8 @@ import {
ProductDetails_product_variants
} from "@saleor/products/types/ProductDetails";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes";
import { StockInput } from "@saleor/types/globalTypes";
import { FormsetAtomicData } from "@saleor/hooks/useFormset";
import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import { ProductVariant } from "../types/ProductVariant";
@ -211,3 +213,12 @@ export function getProductUpdatePageFormData(
trackInventory: !!product?.variants[0]?.trackInventory
};
}
export function mapFormsetStockToStockInput(
stock: FormsetAtomicData<null, string>
): StockInput {
return {
quantity: parseInt(stock.value, 10),
warehouse: stock.id
};
}

View file

@ -9,31 +9,16 @@ import useShop from "@saleor/hooks/useShop";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { useWarehouseList } from "@saleor/warehouses/queries";
import { decimal, maybe } from "../../misc";
import ProductCreatePage, {
ProductCreatePageSubmitData
} from "../components/ProductCreatePage";
import { TypedProductCreateMutation } from "../mutations";
import { ProductCreate } from "../types/ProductCreate";
import {
productListUrl,
productUrl,
ProductAddUrlDialog,
ProductAddUrlQueryParams,
productAddUrl
} from "../urls";
import ProductWarehousesDialog from "../components/ProductWarehousesDialog";
import { productListUrl, productUrl } from "../urls";
interface ProductCreateViewProps {
params: ProductAddUrlQueryParams;
}
export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
params
}) => {
export const ProductCreateView: React.FC = () => {
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
@ -59,20 +44,12 @@ export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
} = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const { result: searchWarehousesOpts } = useWarehouseSearch({
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
first: 50
}
});
const [warehouses, setWarehouses] = React.useState<
SearchWarehouses_search_edges_node[]
>([]);
const [openModal, closeModal] = createDialogActionHandlers<
ProductAddUrlDialog,
ProductAddUrlQueryParams
>(navigate, productAddUrl, params);
const handleBack = () => navigate(productListUrl());
@ -153,7 +130,6 @@ export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
productTypes={maybe(() =>
searchProductTypesOpts.data.search.edges.map(edge => edge.node)
)}
warehouses={warehouses}
onBack={handleBack}
onSubmit={handleSubmit}
saveButtonBarState={productCreateOpts.status}
@ -178,32 +154,9 @@ export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
onWarehouseEdit={() => openModal("edit-stocks")}
/>
<ProductWarehousesDialog
confirmButtonState="default"
disabled={false}
errors={[]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => edge.node
)}
warehousesWithStocks={warehouses.map(warehouse => warehouse.id)}
onConfirm={data => {
setWarehouses(
[
...warehouses,
...data.added.map(
addedId =>
searchWarehousesOpts.data.search.edges.find(
edge => edge.node.id === addedId
).node
)
].filter(warehouse => !data.removed.includes(warehouse.id))
);
closeModal();
}}
warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
}
/>
</>
);

View file

@ -306,7 +306,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
.hasNextPage,
false
)}
onAdd={() => navigate(productAddUrl())}
onAdd={() => navigate(productAddUrl)}
disabled={loading}
products={maybe(() =>
data.products.edges.map(edge => edge.node)

View file

@ -16,9 +16,7 @@ import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import ProductWarehousesDialog from "@saleor/products/components/ProductWarehousesDialog";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import { useAddOrRemoveStocks } from "@saleor/products/mutations";
import { useWarehouseList } from "@saleor/warehouses/queries";
import { getMutationState, maybe } from "../../../misc";
import ProductUpdatePage from "../../components/ProductUpdatePage";
import ProductUpdateOperations from "../../containers/ProductUpdateOperations";
@ -71,24 +69,10 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const { result: searchWarehousesOpts } = useWarehouseSearch({
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
}
});
const [addOrRemoveStocks, addOrRemoveStocksOpts] = useAddOrRemoveStocks({
onCompleted: data => {
if (
data.productVariantStocksCreate.errors.length === 0 &&
data.productVariantStocksDelete.errors.length === 0
) {
notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
closeModal();
}
first: 50
}
});
@ -238,6 +222,11 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
header={maybe(() => product.name)}
placeholderImage={placeholderImg}
product={product}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
variants={maybe(() => product.variants)}
onBack={handleBack}
onDelete={() => openModal("remove")}
@ -282,7 +271,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
onWarehousesEdit={() => openModal("edit-stocks")}
/>
<ActionDialog
open={params.action === "remove"}
@ -333,40 +321,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
/>
</DialogContentText>
</ActionDialog>
{!product?.productType?.hasVariants && (
<ProductWarehousesDialog
confirmButtonState={addOrRemoveStocksOpts.status}
disabled={addOrRemoveStocksOpts.loading}
errors={[
...(addOrRemoveStocksOpts.data
?.productVariantStocksCreate.errors || []),
...(addOrRemoveStocksOpts.data
?.productVariantStocksDelete.errors || [])
]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => edge.node
)}
warehousesWithStocks={
product?.variants[0].stocks.map(
stock => stock.warehouse.id
) || []
}
onConfirm={data =>
addOrRemoveStocks({
variables: {
add: data.added.map(id => ({
quantity: 0,
warehouse: id
})),
remove: data.removed,
variantId: product.variants[0].id
}
})
}
/>
)}
</>
);
}}

View file

@ -7,6 +7,7 @@ import { ProductUpdateVariables } from "@saleor/products/types/ProductUpdate";
import { SimpleProductUpdateVariables } from "@saleor/products/types/SimpleProductUpdate";
import { ReorderEvent } from "@saleor/types";
import { arrayMove } from "react-sortable-hoc";
import { mapFormsetStockToStockInput } from "@saleor/products/utils/data";
export function createUpdateHandler(
product: ProductDetails_product,
@ -40,17 +41,14 @@ export function createUpdateHandler(
} else {
updateSimpleProduct({
...productVariables,
addStocks: [],
deleteStocks: [],
addStocks: data.addStocks.map(mapFormsetStockToStockInput),
deleteStocks: data.removeStocks,
productVariantId: product.variants[0].id,
productVariantInput: {
sku: data.sku,
trackInventory: data.trackInventory
},
updateStocks: data.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
}))
updateStocks: data.updateStocks.map(mapFormsetStockToStockInput)
});
}
};

View file

@ -8,8 +8,7 @@ import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import NotFoundPage from "@saleor/components/NotFoundPage";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import { useWarehouseList } from "@saleor/warehouses/queries";
import { decimal } from "../../misc";
import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog";
import ProductVariantPage, {
@ -28,8 +27,7 @@ import {
ProductVariantEditUrlQueryParams,
ProductVariantEditUrlDialog
} from "../urls";
import ProductWarehousesDialog from "../components/ProductWarehousesDialog";
import { useAddOrRemoveStocks } from "../mutations";
import { mapFormsetStockToStockInput } from "../utils/data";
interface ProductUpdateProps {
variantId: string;
@ -52,28 +50,14 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
setErrors([]);
}, [variantId]);
const { result: searchWarehousesOpts } = useWarehouseSearch({
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
first: 50
}
});
const [addOrRemoveStocks, addOrRemoveStocksOpts] = useAddOrRemoveStocks({
onCompleted: data => {
if (
data.productVariantStocksCreate.errors.length === 0 &&
data.productVariantStocksDelete.errors.length === 0
) {
notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
closeModal();
}
}
});
const [openModal, closeModal] = createDialogActionHandlers<
const [openModal] = createDialogActionHandlers<
ProductVariantEditUrlDialog,
ProductVariantEditUrlQueryParams
>(
@ -151,18 +135,20 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
placeholderImage={placeholderImg}
variant={variant}
header={variant?.name || variant?.sku}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
onAdd={() => navigate(productVariantAddUrl(productId))}
onBack={handleBack}
onDelete={() =>
navigate(
productVariantEditUrl(productId, variantId, {
action: "remove"
})
)
}
onDelete={() => openModal("remove")}
onImageSelect={handleImageSelect}
onSubmit={(data: ProductVariantPageSubmitData) =>
updateVariant.mutate({
addStocks: data.addStocks.map(
mapFormsetStockToStockInput
),
attributes: data.attributes.map(attribute => ({
id: attribute.id,
values: [attribute.value]
@ -170,18 +156,17 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
costPrice: decimal(data.costPrice),
id: variantId,
priceOverride: decimal(data.priceOverride),
removeStocks: data.removeStocks,
sku: data.sku,
stocks: data.stocks.map(stock => ({
quantity: parseInt(stock.value, 10),
warehouse: stock.id
})),
stocks: data.updateStocks.map(
mapFormsetStockToStockInput
),
trackInventory: data.trackInventory
})
}
onVariantClick={variantId => {
navigate(productVariantEditUrl(productId, variantId));
}}
onWarehousesEdit={() => openModal("edit-stocks")}
/>
<ProductVariantDeleteDialog
confirmButtonState={deleteVariant.opts.status}
@ -196,36 +181,6 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
open={params.action === "remove"}
name={data?.productVariant?.name}
/>
<ProductWarehousesDialog
confirmButtonState={addOrRemoveStocksOpts.status}
disabled={addOrRemoveStocksOpts.loading}
errors={[
...(addOrRemoveStocksOpts.data?.productVariantStocksCreate
.errors || []),
...(addOrRemoveStocksOpts.data?.productVariantStocksDelete
.errors || [])
]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => edge.node
)}
warehousesWithStocks={
variant?.stocks.map(stock => stock.warehouse.id) || []
}
onConfirm={data =>
addOrRemoveStocks({
variables: {
add: data.added.map(id => ({
quantity: 0,
warehouse: id
})),
remove: data.removed,
variantId
}
})
}
/>
</>
);
}}

View file

@ -7,10 +7,7 @@ import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { commonMessages } from "@saleor/intl";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import { useWarehouseList } from "@saleor/warehouses/queries";
import { decimal } from "../../misc";
import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData
@ -18,43 +15,25 @@ import ProductVariantCreatePage, {
import { TypedVariantCreateMutation } from "../mutations";
import { TypedProductVariantCreateQuery } from "../queries";
import { VariantCreate } from "../types/VariantCreate";
import {
productUrl,
productVariantEditUrl,
productListUrl,
productVariantAddUrl,
ProductVariantAddUrlDialog,
ProductVariantAddUrlQueryParams
} from "../urls";
import ProductWarehousesDialog from "../components/ProductWarehousesDialog";
import { productUrl, productVariantEditUrl, productListUrl } from "../urls";
interface ProductVariantCreateProps {
params: ProductVariantAddUrlQueryParams;
productId: string;
}
export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
params,
productId
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
const intl = useIntl();
const { result: searchWarehousesOpts } = useWarehouseSearch({
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
first: 50
}
});
const [warehouses, setWarehouses] = React.useState<
SearchWarehouses_search_edges_node[]
>([]);
const [openModal, closeModal] = createDialogActionHandlers<
ProductVariantAddUrlDialog,
ProductVariantAddUrlQueryParams
>(navigate, params => productVariantAddUrl(productId, params), params);
return (
<TypedProductVariantCreateQuery displayLoader variables={{ id: productId }}>
@ -136,37 +115,11 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
onSubmit={handleSubmit}
onVariantClick={handleVariantClick}
saveButtonBarState={variantCreateResult.status}
warehouses={warehouses}
onWarehouseEdit={() => openModal("edit-stocks")}
/>
<ProductWarehousesDialog
confirmButtonState="default"
disabled={false}
errors={[]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => edge.node
)}
warehousesWithStocks={warehouses.map(
warehouse => warehouse.id
)}
onConfirm={data => {
setWarehouses(
[
...warehouses,
...data.added.map(
addedId =>
searchWarehousesOpts.data.search.edges.find(
edge => edge.node.id === addedId
).node
)
].filter(
warehouse => !data.removed.includes(warehouse.id)
)
);
closeModal();
}}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
/>
</>
);

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,6 @@ storiesOf("Views / Products / Create product", module)
onBack={() => undefined}
onSubmit={() => undefined}
saveButtonBarState="default"
onWarehouseEdit={() => undefined}
warehouses={warehouseList}
/>
))
@ -55,7 +54,6 @@ storiesOf("Views / Products / Create product", module)
onBack={() => undefined}
onSubmit={() => undefined}
saveButtonBarState="default"
onWarehouseEdit={() => undefined}
warehouses={undefined}
/>
))
@ -83,7 +81,6 @@ storiesOf("Views / Products / Create product", module)
onBack={() => undefined}
onSubmit={() => undefined}
saveButtonBarState="default"
onWarehouseEdit={() => undefined}
warehouses={warehouseList}
/>
));

View file

@ -10,6 +10,7 @@ import ProductUpdatePage, {
import { product as productFixture } from "@saleor/products/fixtures";
import { ProductUpdatePageFormData } from "@saleor/products/utils/data";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import { warehouseList } from "@saleor/warehouses/fixtures";
import Decorator from "../../Decorator";
const product = productFixture(placeholderImage);
@ -34,11 +35,11 @@ const props: ProductUpdatePageProps = {
onVariantAdd: () => undefined,
onVariantShow: () => undefined,
onVariantsAdd: () => undefined,
onWarehousesEdit: () => undefined,
placeholderImage,
product,
saveButtonBarState: "default",
variants: product.variants
variants: product.variants,
warehouses: warehouseList
};
storiesOf("Views / Products / Product edit", module)

View file

@ -24,7 +24,6 @@ storiesOf("Views / Products / Create product variant", module)
onVariantClick={undefined}
saveButtonBarState="default"
warehouses={warehouseList}
onWarehouseEdit={() => undefined}
/>
))
.add("with errors", () => (
@ -55,7 +54,6 @@ storiesOf("Views / Products / Create product variant", module)
onVariantClick={undefined}
saveButtonBarState="default"
warehouses={warehouseList}
onWarehouseEdit={() => undefined}
/>
))
.add("when loading data", () => (
@ -70,7 +68,6 @@ storiesOf("Views / Products / Create product variant", module)
onVariantClick={undefined}
saveButtonBarState="default"
warehouses={warehouseList}
onWarehouseEdit={() => undefined}
/>
))
.add("add first variant", () => (
@ -88,6 +85,5 @@ storiesOf("Views / Products / Create product variant", module)
onVariantClick={undefined}
saveButtonBarState="default"
warehouses={warehouseList}
onWarehouseEdit={() => undefined}
/>
));

View file

@ -3,6 +3,7 @@ import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import { warehouseList } from "@saleor/warehouses/fixtures";
import ProductVariantPage from "../../../products/components/ProductVariantPage";
import { variant as variantFixture } from "../../../products/fixtures";
import Decorator from "../../Decorator";
@ -23,7 +24,7 @@ storiesOf("Views / Products / Product variant details", module)
onSubmit={() => undefined}
onVariantClick={() => undefined}
saveButtonBarState="default"
onWarehousesEdit={() => undefined}
warehouses={warehouseList}
/>
))
.add("when loading data", () => (
@ -39,7 +40,7 @@ storiesOf("Views / Products / Product variant details", module)
onSubmit={() => undefined}
onVariantClick={() => undefined}
saveButtonBarState="default"
onWarehousesEdit={() => undefined}
warehouses={warehouseList}
/>
))
.add("attribute errors", () => (
@ -71,6 +72,6 @@ storiesOf("Views / Products / Product variant details", module)
message: "Generic form error",
...error
}))}
onWarehousesEdit={() => undefined}
warehouses={warehouseList}
/>
));