Handle form errors before product creation (#2299)

* Handle form errors before product creation

* Update e2e product create tests

* Handle channel errors in product update

* Update changelog

* Fix invalid values of product type picker

* Refactor product create utils

* trigger ci

Co-authored-by: Patryk Andrzejewski <vox3r69@gmail.com>
This commit is contained in:
Dawid 2022-09-23 14:15:59 +02:00 committed by GitHub
parent 9f7c934dec
commit 6738467304
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 391 additions and 82 deletions

View file

@ -10,6 +10,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix invalid values in channel picker - #2313 by @orzechdev
- Fix missing metadata and payment balance on unconfirmed orders - #2314 by @orzechdev
- Fix exit form dialog false positive - #2311 by @orzechdev
- Handle form errors before product creation - #2299 by @orzechdev
- Fix no product error on unconfirmed order lines - #2324 by @orzechdev
## 3.4

View file

@ -19,7 +19,10 @@ import {
fillUpPriceList,
priceInputLists,
} from "../../support/pages/catalog/products/priceListComponent";
import { fillUpCommonFieldsForAllProductTypes } from "../../support/pages/catalog/products/productDetailsPage";
import {
fillUpCommonFieldsForAllProductTypes,
fillUpProductTypeDialog,
} from "../../support/pages/catalog/products/productDetailsPage";
import { selectChannelInDetailsPages } from "../../support/pages/channelsPage";
describe("As an admin I should be able to create product", () => {
@ -156,6 +159,8 @@ describe("As an admin I should be able to create product", () => {
.visit(urlList.products)
.get(PRODUCTS_LIST.createProductBtn)
.click();
fillUpProductTypeDialog(productData);
cy.get(BUTTON_SELECTORS.submit).click();
return fillUpCommonFieldsForAllProductTypes(productData);
}
});

View file

@ -4,7 +4,7 @@ export const COLLECTION_SELECTORS = {
saveButton: "[data-test='button-bar-confirm']",
addProductButton: "[data-test-id='add-product']",
descriptionInput: '[data-test-id="rich-text-editor-description"]',
placeholder: "[data-placeholder]"
placeholder: "[data-placeholder]",
};
export const collectionRow = collectionId =>

View file

@ -1,6 +1,7 @@
export const PRODUCTS_LIST = {
productsList: "[data-test-id*='id']",
productsNames: "[data-test-id='name']",
dialogProductTypeInput: "[data-test-id='dialog-product-type']",
createProductBtn: "[data-test-id='add-product']",
searchProducts: "[placeholder='Search Products...']",
emptyProductRow: "[data-test-id='skeleton']",

View file

@ -16,5 +16,5 @@ export const BUTTON_SELECTORS = {
button: "button",
deleteAssignedItemsConsentCheckbox: '[name="delete-assigned-items-consent"]',
deleteSelectedElementsButton:
'[data-test-id = "delete-selected-elements-icon"]'
'[data-test-id = "delete-selected-elements-icon"]',
};

View file

@ -1,4 +1,5 @@
import { PRODUCT_DETAILS } from "../../../../elements/catalog/products/product-details";
import { PRODUCTS_LIST } from "../../../../elements/catalog/products/products-list";
import { AVAILABLE_CHANNELS_FORM } from "../../../../elements/channels/available-channels-form";
import { BUTTON_SELECTORS } from "../../../../elements/shared/button-selectors";
import { addMetadataField } from "../metadataComponent";
@ -90,6 +91,16 @@ export function fillUpProductGeneralInfo({ name, description, rating }) {
.clearAndType(rating);
}
export function fillUpProductTypeDialog({ productType }) {
const organization = {};
return cy
.fillAutocompleteSelect(PRODUCTS_LIST.dialogProductTypeInput, productType)
.then(selected => {
organization.productType = selected;
return organization;
});
}
export function fillUpProductOrganization({
productType,
category,

View file

@ -250,6 +250,10 @@
"context": "OrderCustomer Fulfillment from Local Warehouse",
"string": "Fulfill from Local Stock"
},
"/yJJvI": {
"context": "dialog header",
"string": "Select a product type"
},
"/z9uo1": {
"context": "order returned success message",
"string": "Successfully returned products!"
@ -7451,6 +7455,10 @@
"context": "card header",
"string": "Sign In"
},
"w+3Q3e": {
"context": "input label",
"string": "Product type"
},
"w+5Djm": {
"string": "Min. Order Weight"
},

View file

@ -8,6 +8,8 @@ import { Autocomplete, ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
export interface ChannelPickerDialogProps {
channelsChoices: Array<Choice<string, string>>;
confirmButtonState: ConfirmButtonTransitionState;
@ -44,20 +46,12 @@ const ChannelPickerDialog: React.FC<ChannelPickerDialogProps> = ({
open={open}
onClose={onClose}
onConfirm={() => onConfirm(choice)}
title={intl.formatMessage({
id: "G/pgG3",
defaultMessage: "Select a channel",
description: "dialog header",
})}
title={intl.formatMessage(messages.selectChannel)}
>
<Autocomplete
choices={result}
fullWidth
label={intl.formatMessage({
defaultMessage: "Channel name",
id: "nKwgxY",
description: "select label",
})}
label={intl.formatMessage(messages.channelName)}
data-test-id="channel-autocomplete"
value={choice}
onChange={e => setChoice(e.target.value)}

View file

@ -0,0 +1,14 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
channelName: {
defaultMessage: "Channel name",
id: "nKwgxY",
description: "select label",
},
selectChannel: {
id: "G/pgG3",
defaultMessage: "Select a channel",
description: "dialog header",
},
});

View file

@ -104,7 +104,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
categories: categoryChoiceList,
collections: collectionChoiceList,
attributeValues,
errors,
errors: apiErrors,
fetchCategories,
fetchCollections,
fetchMoreCategories,
@ -210,6 +210,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
change,
data,
formErrors,
validationErrors,
handlers,
submit,
isSaveDisabled,
@ -218,6 +219,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
// Comparing explicitly to false because `hasVariants` can be undefined
const isSimpleProduct = data.productType?.hasVariants === false;
const errors = [...apiErrors, ...validationErrors];
return (
<Container>
<Backlink href={productListUrl()}>
@ -315,7 +318,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
collections={collections}
data={data}
disabled={loading}
errors={errors}
errors={[...errors, ...channelsErrors]}
fetchCategories={fetchCategories}
fetchCollections={fetchCollections}
fetchMoreCategories={fetchMoreCategories}

View file

@ -25,6 +25,7 @@ import { MetadataFormData } from "@saleor/components/Metadata";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import {
ProductErrorWithAttributesFragment,
ProductTypeQuery,
SearchPagesQuery,
SearchProductsQuery,
@ -55,6 +56,7 @@ import {
import {
validateCostPrice,
validatePrice,
validateProductCreateData,
} from "@saleor/products/utils/validation";
import { PRODUCT_CREATE_FORM_ID } from "@saleor/products/views/ProductCreate/consts";
import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
@ -64,7 +66,7 @@ import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTr
import { RichTextContext } from "@saleor/utils/richText/context";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { createPreorderEndDateChangeHandler } from "../../utils/handlers";
@ -138,6 +140,7 @@ export interface UseProductCreateFormOutput
RichTextProps {
disabled: boolean;
formErrors: FormErrors<ProductCreateData>;
validationErrors: ProductErrorWithAttributesFragment[];
}
export type UseProductCreateFormRenderProps = Omit<
@ -185,6 +188,9 @@ function useProductCreateForm(
opts: UseProductCreateFormOpts,
): UseProductCreateFormOutput {
const intl = useIntl();
const [validationErrors, setValidationErrors] = useState<
ProductErrorWithAttributesFragment[]
>([]);
const defaultInitialFormData: ProductCreateFormData &
Record<"productType", string> = {
category: "",
@ -374,14 +380,39 @@ function useProductCreateForm(
),
});
const handleSubmit = async (data: ProductCreateData) => {
const errors = validateProductCreateData(data);
setValidationErrors(errors);
if (errors.length) {
return errors;
}
return onSubmit(data);
};
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit,
onSubmit: handleSubmit,
});
const submit = async () => handleFormSubmit(await getData());
const submit = async () => {
const errors = await handleFormSubmit(await getData());
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
if (errors.length) {
setIsSubmitDisabled(isSubmitDisabled);
setIsDirty(true);
}
return errors;
};
const {
setExitDialogSubmitRef,
setIsSubmitDisabled,
setIsDirty,
} = useExitFormDialog({
formId: PRODUCT_CREATE_FORM_ID,
});
@ -415,14 +446,20 @@ function useProductCreateForm(
return true;
};
const isSaveDisabled = loading || !onSubmit || !isValid();
setIsSubmitDisabled(isSaveDisabled);
const isSaveDisabled = loading || !onSubmit;
const isSubmitDisabled = isSaveDisabled || !isValid();
useEffect(() => {
setIsSubmitDisabled(isSubmitDisabled);
setIsDirty(true);
}, [isSubmitDisabled]);
return {
change: handleChange,
data,
disabled: isSaveDisabled,
formErrors: form.errors,
validationErrors,
handlers: {
addStock: handleStockAdd,
changeChannelPrice: handleChannelPriceChange,

View file

@ -35,7 +35,7 @@ import { hasLimits, isLimitReached } from "@saleor/utils/limits";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { productAddUrl, ProductListUrlSortField } from "../../urls";
import { ProductListUrlSortField } from "../../urls";
import ProductList from "../ProductList";
import { columnsMessages } from "../ProductList/messages";
import {
@ -62,6 +62,7 @@ export interface ProductListPageProps
limits: RefreshLimitsQuery["shop"]["limits"];
totalGridAttributes: number;
products: RelayToFlat<ProductListQuery["products"]>;
onAdd: () => void;
onExport: () => void;
onColumnQueryChange: (query: string) => void;
}
@ -101,6 +102,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
settings,
tabs,
totalGridAttributes,
onAdd,
onAll,
onColumnQueryChange,
onExport,
@ -229,7 +231,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
options={extensionCreateButtonItems}
data-test-id="add-product"
disabled={limitReached}
href={productAddUrl()}
onClick={onAdd}
>
<FormattedMessage
id="JFmOfi"

View file

@ -10,7 +10,11 @@ import MultiAutocompleteSelectField, {
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType,
} from "@saleor/components/SingleAutocompleteSelectField";
import { ProductErrorFragment } from "@saleor/graphql";
import {
ProductChannelListingErrorFragment,
ProductErrorCode,
ProductErrorFragment,
} from "@saleor/graphql";
import { ChangeEvent } from "@saleor/hooks/useForm";
import { commonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
@ -55,7 +59,7 @@ interface ProductOrganizationProps {
productType?: ProductType;
};
disabled: boolean;
errors: ProductErrorFragment[];
errors: Array<ProductErrorFragment | ProductChannelListingErrorFragment>;
productType?: ProductType;
productTypeInputDisplayValue?: string;
productTypes?: SingleAutocompleteChoiceType[];
@ -98,9 +102,13 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
const intl = useIntl();
const formErrors = getFormErrors(
["productType", "category", "collections"],
["productType", "category", "collections", "isPublished"],
errors,
);
const noCategoryError =
formErrors.isPublished?.code === ProductErrorCode.PRODUCT_WITHOUT_CATEGORY
? formErrors.isPublished
: null;
return (
<Card className={classes.card}>
@ -163,8 +171,11 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
<FormSpacer />
<SingleAutocompleteSelectField
displayValue={categoryInputDisplayValue}
error={!!formErrors.category}
helperText={getProductErrorMessage(formErrors.category, intl)}
error={!!(formErrors.category || noCategoryError)}
helperText={getProductErrorMessage(
formErrors.category || noCategoryError,
intl,
)}
disabled={disabled}
label={intl.formatMessage({
id: "ccXLVi",

View file

@ -0,0 +1,29 @@
import Decorator from "@saleor/storybook/Decorator";
import { mapNodeToChoice } from "@saleor/utils/maps";
import { storiesOf } from "@storybook/react";
import React from "react";
import { productTypesList } from "../../fixtures";
import ProductTypePickerDialog, {
ProductTypePickerDialogProps,
} from "./ProductTypePickerDialog";
const productTypes = mapNodeToChoice(productTypesList);
const props: ProductTypePickerDialogProps = {
productTypes,
confirmButtonState: "default",
fetchProductTypes: () => undefined,
fetchMoreProductTypes: {
hasMore: false,
loading: false,
onFetchMore: () => undefined,
},
onClose: () => undefined,
onConfirm: () => undefined,
open: true,
};
storiesOf("Views / Products / Product type dialog", module)
.addDecorator(Decorator)
.add("default", () => <ProductTypePickerDialog {...props} />);

View file

@ -0,0 +1,70 @@
import ActionDialog from "@saleor/components/ActionDialog";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType,
} from "@saleor/components/SingleAutocompleteSelectField";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { FetchMoreProps } from "@saleor/types";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
export interface ProductTypePickerDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
productTypes?: SingleAutocompleteChoiceType[];
fetchProductTypes: (data: string) => void;
fetchMoreProductTypes: FetchMoreProps;
onClose: () => void;
onConfirm: (choice: string) => void;
}
const ProductTypePickerDialog: React.FC<ProductTypePickerDialogProps> = ({
confirmButtonState,
open,
productTypes,
fetchProductTypes,
fetchMoreProductTypes,
onClose,
onConfirm,
}) => {
const intl = useIntl();
const [choice, setChoice] = useStateFromProps("");
const productTypeDisplayValue = productTypes.find(
productType => productType.value === choice,
)?.label;
useModalDialogOpen(open, {
onClose: () => {
setChoice("");
fetchProductTypes("");
},
});
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={() => onConfirm(choice)}
title={intl.formatMessage(messages.selectProductType)}
disabled={!choice}
>
<SingleAutocompleteSelectField
displayValue={productTypeDisplayValue}
name="productType"
label={intl.formatMessage(messages.productType)}
choices={productTypes}
value={choice}
onChange={e => setChoice(e.target.value)}
fetchChoices={fetchProductTypes}
data-test-id="dialog-product-type"
{...fetchMoreProductTypes}
/>
</ActionDialog>
);
};
ProductTypePickerDialog.displayName = "ProductTypePickerDialog";
export default ProductTypePickerDialog;

View file

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

View file

@ -0,0 +1,14 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
productType: {
id: "w+3Q3e",
defaultMessage: "Product type",
description: "input label",
},
selectProductType: {
id: "/yJJvI",
defaultMessage: "Select a product type",
description: "dialog header",
},
});

View file

@ -462,7 +462,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
collectionsInputDisplayValue={selectedCollections}
data={data}
disabled={disabled}
errors={errors}
errors={[...errors, ...channelsErrors]}
fetchCategories={fetchCategories}
fetchCollections={fetchCollections}
fetchMoreCategories={fetchMoreCategories}

View file

@ -469,11 +469,12 @@ function useProductUpdateForm(
return true;
};
const isSaveDisabled = disabled || !isValid();
const isSaveDisabled = disabled;
const isSubmitDisabled = isSaveDisabled || !isValid();
useEffect(() => {
setIsSubmitDisabled(isSaveDisabled);
}, [isSaveDisabled]);
setIsSubmitDisabled(isSubmitDisabled);
}, [isSubmitDisabled]);
return {
change: handleChange,

View file

@ -7,6 +7,7 @@ import {
ProductVariantFragment,
WeightUnitsEnum,
} from "@saleor/graphql";
import { ProductType } from "@saleor/sdk/dist/apollo/types";
import { RelayToFlat } from "@saleor/types";
import { warehouseList } from "@saleor/warehouses/fixtures";
@ -3531,3 +3532,24 @@ export const variantProductImages = (placeholderImage: string) =>
variant(placeholderImage).product.media;
export const variantSiblings = (placeholderImage: string) =>
variant(placeholderImage).product.variants;
export const productTypesList: Array<Pick<
ProductType,
"id" | "name" | "hasVariants"
>> = [
{
hasVariants: true,
id: "UHJvZHVjdFR5cGU6Nw==",
name: "Salt",
},
{
hasVariants: true,
id: "UHJvZHVjdFR5cGU6Nw==",
name: "Sugar",
},
{
hasVariants: true,
id: "UHJvZHVjdFR5cGU6Nw==",
name: "Mushroom",
},
];

View file

@ -23,7 +23,11 @@ export const productAddUrl = (params?: ProductCreateUrlQueryParams) =>
productAddPath + "?" + stringifyQs(params);
export const productListPath = productSection;
export type ProductListUrlDialog = "delete" | "export" | TabActionDialog;
export type ProductListUrlDialog =
| "delete"
| "export"
| "create-product"
| TabActionDialog;
export enum ProductListUrlFiltersEnum {
priceFrom = "priceFrom",
priceTo = "priceTo",
@ -82,8 +86,12 @@ export type ProductUrlQueryParams = BulkAction &
Dialog<ProductUrlDialog> &
SingleAction;
export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction;
export interface ProductCreateUrlProductType {
"product-type-id"?: string;
}
export type ProductCreateUrlQueryParams = Dialog<ProductCreateUrlDialog> &
SingleAction;
SingleAction &
ProductCreateUrlProductType;
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -1,5 +1,36 @@
import {
ProductErrorCode,
ProductErrorWithAttributesFragment,
} from "@saleor/graphql";
import { ProductCreateData } from "../components/ProductCreatePage";
export const validatePrice = (price: string) =>
price === "" || parseInt(price, 10) < 0;
export const validateCostPrice = (price: string) =>
price !== "" && parseInt(price, 10) < 0;
const createEmptyRequiredError = (
field: string,
): ProductErrorWithAttributesFragment => ({
__typename: "ProductError",
code: ProductErrorCode.REQUIRED,
field,
message: null,
attributes: [],
});
export const validateProductCreateData = (data: ProductCreateData) => {
let errors: ProductErrorWithAttributesFragment[] = [];
if (!data.productType) {
errors = [...errors, createEmptyRequiredError("productType")];
}
if (!data.name) {
errors = [...errors, createEmptyRequiredError("name")];
}
return errors;
};

View file

@ -8,6 +8,8 @@ import {
VALUES_PAGINATE_BY,
} from "@saleor/config";
import {
ProductChannelListingErrorFragment,
ProductErrorWithAttributesFragment,
useFileUploadMutation,
useProductChannelListingUpdateMutation,
useProductCreateMutation,
@ -24,6 +26,7 @@ import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import { getMutationErrors } from "@saleor/misc";
import ProductCreatePage, {
ProductCreateData,
} from "@saleor/products/components/ProductCreatePage";
@ -62,9 +65,15 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
const [productCreateComplete, setProductCreateComplete] = React.useState(
false,
);
const [selectedProductTypeId, setSelectedProductTypeId] = React.useState<
string
>();
const selectedProductTypeId = params["product-type-id"];
const handleSelectProductType = (productTypeId: string) =>
navigate(
productAddUrl({
...params,
"product-type-id": productTypeId,
}),
);
const [openModal, closeModal] = createDialogActionHandlers<
ProductCreateUrlDialog,
@ -282,6 +291,15 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
updateChannelsOpts.loading ||
updateVariantChannelsOpts.loading;
const channelsErrors = [
...getMutationErrors(updateVariantChannelsOpts),
...getMutationErrors(updateChannelsOpts),
] as ProductChannelListingErrorFragment[];
const errors = [
...getMutationErrors(productCreateOpts),
...getMutationErrors(productVariantCreateOpts),
] as ProductErrorWithAttributesFragment[];
return (
<>
<WindowTitle
@ -318,14 +336,8 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
[]
}
loading={loading}
channelsErrors={
updateVariantChannelsOpts.data?.productVariantChannelListingUpdate
?.errors
}
errors={[
...(productCreateOpts.data?.productCreate.errors || []),
...(productVariantCreateOpts.data?.productVariantCreate.errors || []),
]}
channelsErrors={channelsErrors}
errors={errors}
fetchCategories={searchCategory}
fetchCollections={searchCollection}
fetchProductTypes={searchProductTypes}
@ -362,7 +374,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
fetchMoreAttributeValues={fetchMoreAttributeValues}
onCloseDialog={() => navigate(productAddUrl())}
selectedProductType={selectedProductType?.productType}
onSelectProductType={id => setSelectedProductTypeId(id)}
onSelectProductType={handleSelectProductType}
onAttributeSelectBlur={searchAttributeReset}
/>
</>

View file

@ -45,7 +45,9 @@ import {
getAttributeIdFromColumnValue,
isAttributeColumnValue,
} from "@saleor/products/components/ProductListPage/utils";
import ProductTypePickerDialog from "@saleor/products/components/ProductTypePickerDialog";
import {
productAddUrl,
productListUrl,
ProductListUrlDialog,
ProductListUrlQueryParams,
@ -324,6 +326,20 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
},
});
const {
loadMore: loadMoreDialogProductTypes,
search: searchDialogProductTypes,
result: searchDialogProductTypesOpts,
} = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA,
});
const fetchMoreDialogProductTypes = {
hasMore: searchDialogProductTypesOpts.data?.search?.pageInfo?.hasNextPage,
loading: searchDialogProductTypesOpts.loading,
onFetchMore: loadMoreDialogProductTypes,
};
const filterOpts = getFilterOpts(
params,
(mapEdgesToItems(initialFilterAttributes?.attributes) || []).filter(
@ -393,6 +409,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
onColumnQueryChange={availableInGridAttributesOpts.search}
onFetchMore={availableInGridAttributesOpts.loadMore}
onUpdateListSettings={updateListSettings}
onAdd={() => openModal("create-product")}
onAll={resetFilters}
toolbar={
<IconButton
@ -499,6 +516,23 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
<ProductTypePickerDialog
confirmButtonState="success"
open={params.action === "create-product"}
productTypes={mapNodeToChoice(
mapEdgesToItems(searchDialogProductTypesOpts?.data?.search),
)}
fetchProductTypes={searchDialogProductTypes}
fetchMoreProductTypes={fetchMoreDialogProductTypes}
onClose={closeModal}
onConfirm={productTypeId =>
navigate(
productAddUrl({
"product-type-id": productTypeId,
}),
)
}
/>
</PaginatorContext.Provider>
);
};

View file

@ -208242,20 +208242,18 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -210688,20 +210686,19 @@ exports[`Storyshots Views / Products / Product list limits reached 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="true"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-disabled-id MuiButton-containedPrimary-id MuiButton-disabled-id MuiButtonBase-disabled-id"
href="/products/add"
role="button"
disabled=""
style="width:100%"
tabindex="-1"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -213217,20 +213214,18 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -213791,20 +213786,18 @@ exports[`Storyshots Views / Products / Product list no channels 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -216237,20 +216230,18 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -216649,20 +216640,18 @@ exports[`Storyshots Views / Products / Product list no limits 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -219095,20 +219084,18 @@ exports[`Storyshots Views / Products / Product list with data 1`] = `
data-test-id="add-product"
role="group"
>
<a
aria-disabled="false"
<button
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButtonGroup-groupedOutlinedHorizontal-id MuiButtonGroup-groupedOutlined-id MuiButton-containedPrimary-id"
href="/products/add"
role="button"
style="width:100%"
tabindex="0"
type="button"
>
<span
class="MuiButton-label-id"
>
Create Product
</span>
</a>
</button>
</div>
</div>
</div>
@ -221439,6 +221426,12 @@ exports[`Storyshots Views / Products / Product list with data 1`] = `
</div>
`;
exports[`Storyshots Views / Products / Product type dialog default 1`] = `
<div
style="padding:24px"
/>
`;
exports[`Storyshots Views / Products / Product variant details attribute errors 1`] = `
<div
style="padding:24px"

View file

@ -1,6 +1,7 @@
import {
BulkProductErrorFragment,
CollectionErrorFragment,
ProductChannelListingErrorFragment,
ProductErrorCode,
ProductErrorFragment,
} from "@saleor/graphql";
@ -74,7 +75,12 @@ const messages = defineMessages({
function getProductErrorMessage(
err:
| Omit<ProductErrorFragment | CollectionErrorFragment, "__typename">
| Omit<
| ProductErrorFragment
| CollectionErrorFragment
| ProductChannelListingErrorFragment,
"__typename"
>
| undefined,
intl: IntlShape,
): string {