Add simple product stock management
This commit is contained in:
parent
db3b51f931
commit
4b661c9bff
12 changed files with 150 additions and 42 deletions
|
@ -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(
|
||||
|
|
|
@ -46,7 +46,7 @@ export function searchInCommands(
|
|||
{
|
||||
label: intl.formatMessage(messages.createProduct),
|
||||
onClick: () => {
|
||||
navigate(productAddUrl);
|
||||
navigate(productAddUrl());
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
@ -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,24 +172,41 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderCollection(stocks, stock => (
|
||||
<TableRow>
|
||||
<TableCell className={classes.colName}>{stock.label}</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<TextField
|
||||
className={classes.inputComponent}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
className: classes.input,
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
onChange={event => onChange(stock.id, event.target.value)}
|
||||
value={stock.value}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{renderCollection(
|
||||
stocks,
|
||||
stock => (
|
||||
<TableRow>
|
||||
<TableCell className={classes.colName}>{stock.label}</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<TextField
|
||||
className={classes.inputComponent}
|
||||
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}>
|
||||
<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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in a new issue