Add simple product stock management

This commit is contained in:
dominik-zeglen 2020-03-25 14:06:14 +01:00
parent db3b51f931
commit 4b661c9bff
12 changed files with 150 additions and 42 deletions

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

@ -28,6 +28,7 @@ import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { FetchMoreProps } from "../../../types";
import {
createAttributeChangeHandler,
@ -81,6 +82,7 @@ interface ProductCreatePageProps {
}>;
header: string;
saveButtonBarState: ConfirmButtonTransitionState;
warehouses: SearchWarehouses_search_edges_node[];
fetchCategories: (data: string) => void;
fetchCollections: (data: string) => void;
fetchProductTypes: (data: string) => void;
@ -103,6 +105,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
header,
productTypes: productTypeChoiceList,
saveButtonBarState,
warehouses,
onBack,
fetchProductTypes,
onSubmit,
@ -116,7 +119,18 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
data: attributes,
set: setAttributeData
} = useFormset<ProductAttributeInputData>([]);
const { change: changeStockData, data: stocks } = useFormset<null>([]);
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)]);
// Ensures that it will not change after component rerenders, because it
// generates different block keys and it causes editor to lose its content.
@ -248,7 +262,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onFormDataChange={change}
errors={errors}
stocks={stocks}
onWarehouseEdit={onWarehouseEdit}
onWarehousesEdit={onWarehouseEdit}
/>
<CardSpacer />
</>

View file

@ -21,6 +21,7 @@ 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";
export type ProductStockInput = FormsetAtomicData<null, string>;
export interface ProductStockFormData {
@ -35,7 +36,7 @@ export interface ProductStocksProps {
stocks: ProductStockInput[];
onChange: FormsetChange;
onFormDataChange: FormChange;
onWarehousesEdit: () => undefined;
onWarehousesEdit: () => void;
}
const useStyles = makeStyles(
@ -171,7 +172,9 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
</TableRow>
</TableHead>
<TableBody>
{renderCollection(stocks, stock => (
{renderCollection(
stocks,
stock => (
<TableRow>
<TableCell className={classes.colName}>{stock.label}</TableCell>
<TableCell className={classes.colQuantity}>
@ -188,7 +191,22 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
/>
</TableCell>
</TableRow>
))}
),
() => (
<TableRow>
<TableCell colSpan={2}>
<FormattedMessage
defaultMessage={
"This product doesn't have any stock. You can add it <l>here</l>."
}
values={{
l: str => <Link onClick={onWarehousesEdit}>{str}</Link>
}}
/>
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>

View file

@ -15,7 +15,6 @@ import ConfirmButton, {
import { buttonMessages } from "@saleor/intl";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import Skeleton from "@saleor/components/Skeleton";
import { Product_variants_stocks } from "@saleor/products/types/Product";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import { isSelected, toggle } from "@saleor/utils/lists";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -44,8 +43,8 @@ export interface ProductWarehousesDialogProps {
disabled: boolean;
errors: Array<BulkStockErrorFragment | StockErrorFragment>;
open: boolean;
stocks: Product_variants_stocks[];
warehouses: SearchWarehouses_search_edges_node[];
warehousesWithStocks: string[];
onClose: () => void;
onConfirm: (data: DiffData<string>) => void;
}
@ -57,18 +56,18 @@ const ProductWarehousesDialog: React.FC<ProductWarehousesDialogProps> = ({
onClose,
onConfirm,
open,
stocks,
warehousesWithStocks,
warehouses
}) => {
const classes = useStyles({});
const intl = useIntl();
const initial = stocks?.map(stock => stock.warehouse.id) || [];
const [selectedWarehouses, setSelectedWarehouses] = useStateFromProps(
initial
warehousesWithStocks || []
);
const handleConfirm = () => onConfirm(diff(initial, selectedWarehouses));
const handleConfirm = () =>
onConfirm(diff(warehousesWithStocks, selectedWarehouses));
return (
<Dialog onClose={onClose} maxWidth="sm" fullWidth open={open}>

View file

@ -18,9 +18,10 @@ import {
ProductUrlQueryParams,
productVariantAddPath,
productVariantEditPath,
ProductVariantEditUrlQueryParams
ProductVariantEditUrlQueryParams,
ProductAddUrlQueryParams
} from "./urls";
import ProductCreate from "./views/ProductCreate";
import ProductCreateComponent from "./views/ProductCreate";
import ProductImageComponent from "./views/ProductImage";
import ProductListComponent from "./views/ProductList";
import ProductUpdateComponent from "./views/ProductUpdate";
@ -92,6 +93,13 @@ const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({
/>
);
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

@ -302,6 +302,7 @@ export const productCreateMutation = gql`
$sku: String
$stockQuantity: Int
$seo: SeoInput
$stocks: [StockInput!]!
) {
productCreate(
input: {
@ -318,6 +319,7 @@ export const productCreateMutation = gql`
sku: $sku
quantity: $stockQuantity
seo: $seo
stocks: $stocks
}
) {
errors: productErrors {

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueInput, SeoInput, ProductErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes";
import { AttributeValueInput, SeoInput, StockInput, ProductErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ProductCreate
@ -211,4 +211,5 @@ export interface ProductCreateVariables {
sku?: string | null;
stockQuantity?: number | null;
seo?: SeoInput | null;
stocks: StockInput[];
}

View file

@ -17,7 +17,10 @@ import {
const productSection = "/products/";
export const productAddPath = urlJoin(productSection, "add");
export const productAddUrl = productAddPath;
export type ProductAddUrlDialog = "edit-stocks";
export type ProductAddUrlQueryParams = Dialog<ProductAddUrlDialog>;
export const productAddUrl = (params?: ProductAddUrlQueryParams): string =>
productAddPath + "?" + stringifyQs(params);
export const productListPath = productSection;
export type ProductListUrlDialog =

View file

@ -9,19 +9,31 @@ 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 { decimal, maybe } from "../../misc";
import ProductCreatePage, {
ProductCreatePageSubmitData
} from "../components/ProductCreatePage";
import { TypedProductCreateMutation } from "../mutations";
import { ProductCreate } from "../types/ProductCreate";
import { productListUrl, productUrl } from "../urls";
import {
productListUrl,
productUrl,
ProductAddUrlDialog,
ProductAddUrlQueryParams,
productAddUrl
} from "../urls";
import ProductWarehousesDialog from "../components/ProductWarehousesDialog";
interface ProductUpdateProps {
id: string;
interface ProductCreateViewProps {
params: ProductAddUrlQueryParams;
}
export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
params
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
@ -47,6 +59,24 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
} = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMoreWarehouses,
search: searchWarehouses,
result: searchWarehousesOpts
} = useWarehouseSearch({
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
}
});
const [warehouses, setWarehouses] = React.useState<
SearchWarehouses_search_edges_node[]
>([]);
const [openModal, closeModal] = createDialogActionHandlers<
ProductAddUrlDialog,
ProductAddUrlQueryParams
>(navigate, productAddUrl, params);
const handleBack = () => navigate(productListUrl());
@ -88,8 +118,10 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
title: formData.seoTitle
},
sku: formData.sku,
stockQuantity:
formData.stockQuantity !== null ? formData.stockQuantity : 0
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
}))
}
});
};
@ -124,6 +156,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
productTypes={maybe(() =>
searchProductTypesOpts.data.search.edges.map(edge => edge.node)
)}
warehouses={warehouses}
onBack={handleBack}
onSubmit={handleSubmit}
saveButtonBarState={productCreateOpts.status}
@ -148,6 +181,32 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
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();
}}
/>
</>
);
@ -155,4 +214,4 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
</TypedProductCreateMutation>
);
};
export default ProductUpdate;
export default ProductCreateView;

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

@ -384,11 +384,15 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
?.productVariantStocksDelete.errors || [])
]}
onClose={closeModal}
stocks={product?.variants[0].stocks || []}
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: {