Extract variant update form outside page

This commit is contained in:
dominik-zeglen 2020-10-21 14:09:06 +02:00
parent 78bb07e948
commit 37541c24e1
5 changed files with 285 additions and 194 deletions

View file

@ -17,6 +17,7 @@ import {
createAttributeMultiChangeHandler
} from "@saleor/products/utils/handlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";

View file

@ -16,13 +16,12 @@ import { VariantAttributeInputData } from "../ProductVariantAttributes";
export interface ProductVariantCreateFormData extends MetadataFormData {
costPrice: string;
price: string;
quantity: string;
sku: string;
trackInventory: boolean;
weight: string;
}
export interface ProductVariantCreateData extends ProductVariantCreateFormData {
attributes: FormsetData<VariantAttributeInputData>;
attributes: FormsetData<VariantAttributeInputData, string>;
stocks: ProductStockInput[];
}
@ -33,7 +32,7 @@ export interface UseProductVariantCreateFormOpts {
export interface UseProductVariantCreateFormResult {
change: FormChange;
data: ProductVariantCreateData;
handlers: Record<"changeStock" | "selectAttribute", FormsetChange> &
handlers: Record<"changeStock" | "selectAttribute", FormsetChange<string>> &
Record<"addStock" | "deleteStock", (id: string) => void> & {
changeMetadata: FormChange;
};
@ -53,7 +52,6 @@ const initial: ProductVariantCreateFormData = {
metadata: [],
price: "",
privateMetadata: [],
quantity: "0",
sku: "",
trackInventory: true,
weight: ""

View file

@ -2,7 +2,6 @@ import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import { MetadataFormData } from "@saleor/components/Metadata";
import Metadata from "@saleor/components/Metadata/Metadata";
@ -11,19 +10,9 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import { FormsetData } from "@saleor/hooks/useFormset";
import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate";
import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { ReorderAction } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff";
import React from "react";
import { maybe } from "../../../misc";
@ -37,6 +26,7 @@ import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog"
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantSetDefault from "../ProductVariantSetDefault";
import ProductVariantUpdateForm from "./form";
export interface ProductVariantPageFormData extends MetadataFormData {
costPrice: string;
@ -97,77 +87,17 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onSetDefaultVariant,
onWarehouseConfigure
}) => {
const attributeInput = React.useMemo(
() => getAttributeInputFromVariant(variant),
[variant]
);
const stockInput = React.useMemo(() => getStockInputFromVariant(variant), [
variant
]);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset(stockInput);
const [isModalOpened, setModalStatus] = React.useState(false);
const toggleModal = () => setModalStatus(!isModalOpened);
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const variantImages = maybe(() => variant.images.map(image => image.id), []);
const productImages = maybe(() =>
variant.product.images.sort((prev, next) =>
const variantImages = variant?.images?.map(image => image.id) || [];
const productImages =
variant?.product?.images?.sort((prev, next) =>
prev.sortOrder > next.sortOrder ? 1 : -1
)
);
const images = maybe(() =>
productImages
.filter(image => variantImages.indexOf(image.id) !== -1)
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1))
);
const initialForm: ProductVariantPageFormData = {
costPrice: variant?.costPrice?.amount.toString() || "",
metadata: variant?.metadata?.map(mapMetadataItemToInput),
price: variant?.price?.amount.toString() || "",
privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput),
sku: variant?.sku || "",
trackInventory: !!variant?.trackInventory,
weight: variant?.weight?.value.toString() || ""
};
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);
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({
...data,
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
attributes,
metadata,
privateMetadata,
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
});
};
) || [];
const images = productImages
.filter(image => variantImages.indexOf(image.id) !== -1)
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
return (
<>
@ -182,116 +112,95 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
/>
)}
</PageHeader>
<Form initial={initialForm} onSubmit={handleSubmit} confirmLeave>
{({ change, data, hasChanged, submit, triggerChange }) => {
const handleAttributeChange: FormsetChange = (id, value) => {
changeAttributeData(id, value);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(change);
return (
<>
<Grid variant="inverted">
<div>
<ProductVariantNavigation
current={variant ? variant.id : undefined}
defaultVariantId={defaultVariantId}
fallbackThumbnail={maybe(
() => variant.product.thumbnail.url
)}
variants={maybe(() => variant.product.variants)}
onAdd={onAdd}
onRowClick={(variantId: string) => {
if (variant) {
return onVariantClick(variantId);
}
}}
onReorder={onVariantReorder}
/>
</div>
<div>
<ProductVariantAttributes
attributes={attributes}
disabled={loading}
errors={errors}
onChange={handleAttributeChange}
/>
<CardSpacer />
<ProductVariantImages
disabled={loading}
images={images}
placeholderImage={placeholderImage}
onImageAdd={toggleModal}
/>
<CardSpacer />
<ProductVariantPrice
errors={errors}
data={data}
currencySymbol={
variant && variant.price
? variant.price.currency
: variant && variant.costPrice
? variant.costPrice.currency
: ""
<ProductVariantUpdateForm
variant={variant}
onSubmit={onSubmit}
warehouses={warehouses}
>
{({ change, data, handlers, hasChanged, submit }) => (
<>
<Grid variant="inverted">
<div>
<ProductVariantNavigation
current={variant ? variant.id : undefined}
defaultVariantId={defaultVariantId}
fallbackThumbnail={maybe(
() => variant.product.thumbnail.url
)}
variants={maybe(() => variant.product.variants)}
onAdd={onAdd}
onRowClick={(variantId: string) => {
if (variant) {
return onVariantClick(variantId);
}
loading={loading}
onChange={change}
/>
<CardSpacer />
<ProductShipping
data={data}
disabled={loading}
errors={errors}
weightUnit={variant?.weight?.unit || defaultWeightUnit}
onChange={change}
/>
<CardSpacer />
<ProductStocks
data={data}
disabled={loading}
hasVariants={true}
errors={errors}
stocks={stocks}
warehouses={warehouses}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
onWarehouseConfigure={onWarehouseConfigure}
/>
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
</div>
</Grid>
<SaveButtonBar
disabled={loading || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
/>
</>
);
}}
</Form>
}}
onReorder={onVariantReorder}
/>
</div>
<div>
<ProductVariantAttributes
attributes={data.attributes}
disabled={loading}
errors={errors}
onChange={handlers.selectAttribute}
/>
<CardSpacer />
<ProductVariantImages
disabled={loading}
images={images}
placeholderImage={placeholderImage}
onImageAdd={toggleModal}
/>
<CardSpacer />
<ProductVariantPrice
data={data}
errors={errors}
currencySymbol={
variant && variant.price
? variant.price.currency
: variant && variant.costPrice
? variant.costPrice.currency
: ""
}
loading={loading}
onChange={change}
/>
<CardSpacer />
<ProductShipping
data={data}
disabled={loading}
errors={errors}
weightUnit={variant?.weight?.unit || defaultWeightUnit}
onChange={change}
/>
<CardSpacer />
<ProductStocks
data={data}
disabled={loading}
hasVariants={true}
errors={errors}
stocks={data.stocks}
warehouses={warehouses}
onChange={handlers.changeStock}
onFormDataChange={change}
onWarehouseStockAdd={handlers.addStock}
onWarehouseStockDelete={handlers.deleteStock}
onWarehouseConfigure={onWarehouseConfigure}
/>
<CardSpacer />
<Metadata data={data} onChange={handlers.changeMetadata} />
</div>
</Grid>
<SaveButtonBar
disabled={loading || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
/>
</>
)}
</ProductVariantUpdateForm>
</Container>
{variant && (
<ProductVariantImageSelectDialog

View file

@ -0,0 +1,180 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff";
import React from "react";
import handleFormSubmit from "../../../utils/handlers/handleFormSubmit";
import { ProductStockInput } from "../ProductStocks";
import { VariantAttributeInputData } from "../ProductVariantAttributes";
export interface ProductVariantUpdateFormData extends MetadataFormData {
costPrice: string;
price: string;
sku: string;
trackInventory: boolean;
weight: string;
}
export interface ProductVariantUpdateData extends ProductVariantUpdateFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
stocks: ProductStockInput[];
}
export interface ProductVariantUpdateSubmitData
extends ProductVariantUpdateFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
addStocks: ProductStockInput[];
updateStocks: ProductStockInput[];
removeStocks: string[];
}
export interface UseProductVariantUpdateFormOpts {
warehouses: SearchWarehouses_search_edges_node[];
}
export interface UseProductVariantUpdateFormResult {
change: FormChange;
data: ProductVariantUpdateData;
handlers: Record<"changeStock" | "selectAttribute", FormsetChange> &
Record<"addStock" | "deleteStock", (id: string) => void> & {
changeMetadata: FormChange;
};
hasChanged: boolean;
submit: () => void;
}
export interface ProductVariantUpdateFormProps
extends UseProductVariantUpdateFormOpts {
children: (props: UseProductVariantUpdateFormResult) => React.ReactNode;
variant: ProductVariant;
onSubmit: (data: ProductVariantUpdateSubmitData) => Promise<any[]>;
}
function useProductVariantUpdateForm(
variant: ProductVariant,
onSubmit: (data: ProductVariantUpdateSubmitData) => Promise<any[]>,
opts: UseProductVariantUpdateFormOpts
): UseProductVariantUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const attributeInput = getAttributeInputFromVariant(variant);
const stockInput = getStockInputFromVariant(variant);
const initial: ProductVariantUpdateFormData = {
costPrice: variant?.costPrice?.amount.toString() || "",
metadata: variant?.metadata?.map(mapMetadataItemToInput),
price: variant?.price?.amount.toString() || "",
privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput),
sku: variant?.sku || "",
trackInventory: variant?.trackInventory,
weight: variant?.weight?.value.toString() || ""
};
const form = useForm(initial);
const attributes = useFormset(attributeInput);
const stocks = useFormset(stockInput);
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange);
const handleAttributeChange: FormsetChange = (id, value) => {
attributes.change(id, value);
triggerChange();
};
const handleStockAdd = (id: string) => {
triggerChange();
stocks.add({
data: null,
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
value: "0"
});
};
const handleStockChange = (id: string, value: string) => {
triggerChange();
stocks.change(id, value);
};
const handleStockDelete = (id: string) => {
triggerChange();
stocks.remove(id);
};
const metadata = isMetadataModified ? form.data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? form.data.privateMetadata
: undefined;
const dataStocks = stocks.data.map(stock => stock.id);
const variantStocks = variant?.stocks.map(stock => stock.warehouse.id) || [];
const stockDiff = diff(variantStocks, dataStocks);
const addStocks = stocks.data.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
);
const updateStocks = stocks.data.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
);
const data: ProductVariantUpdateData = {
...form.data,
attributes: attributes.data,
stocks: stocks.data
};
const submitData: ProductVariantUpdateSubmitData = {
...form.data,
addStocks,
attributes: attributes.data,
metadata,
privateMetadata,
removeStocks: stockDiff.removed,
updateStocks
};
const submit = () => handleFormSubmit(submitData, onSubmit, setChanged);
return {
change: handleChange,
data,
handlers: {
addStock: handleStockAdd,
changeMetadata,
changeStock: handleStockChange,
deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange
},
hasChanged: changed,
submit
};
}
const ProductVariantUpdateForm: React.FC<ProductVariantUpdateFormProps> = ({
children,
variant,
onSubmit,
...rest
}) => {
const props = useProductVariantUpdateForm(variant, onSubmit, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
ProductVariantUpdateForm.displayName = "ProductVariantUpdateForm";
export default ProductVariantUpdateForm;

View file

@ -1,9 +1,10 @@
async function handleFormSubmit<T>(
data: T,
onSubmit: (data: T) => Promise<boolean>,
onSubmit: (data: T) => Promise<any[]>,
setChanged: (changed: boolean) => void
): Promise<boolean> {
const ok = await onSubmit(data);
const errors = await onSubmit(data);
const ok = errors.length === 0;
if (ok) {
setChanged(false);
@ -11,3 +12,5 @@ async function handleFormSubmit<T>(
return ok;
}
export default handleFormSubmit;