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:
parent
9f7c934dec
commit
6738467304
26 changed files with 391 additions and 82 deletions
|
@ -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 invalid values in channel picker - #2313 by @orzechdev
|
||||||
- Fix missing metadata and payment balance on unconfirmed orders - #2314 by @orzechdev
|
- Fix missing metadata and payment balance on unconfirmed orders - #2314 by @orzechdev
|
||||||
- Fix exit form dialog false positive - #2311 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
|
- Fix no product error on unconfirmed order lines - #2324 by @orzechdev
|
||||||
|
|
||||||
## 3.4
|
## 3.4
|
||||||
|
|
|
@ -19,7 +19,10 @@ import {
|
||||||
fillUpPriceList,
|
fillUpPriceList,
|
||||||
priceInputLists,
|
priceInputLists,
|
||||||
} from "../../support/pages/catalog/products/priceListComponent";
|
} 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";
|
import { selectChannelInDetailsPages } from "../../support/pages/channelsPage";
|
||||||
|
|
||||||
describe("As an admin I should be able to create product", () => {
|
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)
|
.visit(urlList.products)
|
||||||
.get(PRODUCTS_LIST.createProductBtn)
|
.get(PRODUCTS_LIST.createProductBtn)
|
||||||
.click();
|
.click();
|
||||||
|
fillUpProductTypeDialog(productData);
|
||||||
|
cy.get(BUTTON_SELECTORS.submit).click();
|
||||||
return fillUpCommonFieldsForAllProductTypes(productData);
|
return fillUpCommonFieldsForAllProductTypes(productData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ export const COLLECTION_SELECTORS = {
|
||||||
saveButton: "[data-test='button-bar-confirm']",
|
saveButton: "[data-test='button-bar-confirm']",
|
||||||
addProductButton: "[data-test-id='add-product']",
|
addProductButton: "[data-test-id='add-product']",
|
||||||
descriptionInput: '[data-test-id="rich-text-editor-description"]',
|
descriptionInput: '[data-test-id="rich-text-editor-description"]',
|
||||||
placeholder: "[data-placeholder]"
|
placeholder: "[data-placeholder]",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collectionRow = collectionId =>
|
export const collectionRow = collectionId =>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const PRODUCTS_LIST = {
|
export const PRODUCTS_LIST = {
|
||||||
productsList: "[data-test-id*='id']",
|
productsList: "[data-test-id*='id']",
|
||||||
productsNames: "[data-test-id='name']",
|
productsNames: "[data-test-id='name']",
|
||||||
|
dialogProductTypeInput: "[data-test-id='dialog-product-type']",
|
||||||
createProductBtn: "[data-test-id='add-product']",
|
createProductBtn: "[data-test-id='add-product']",
|
||||||
searchProducts: "[placeholder='Search Products...']",
|
searchProducts: "[placeholder='Search Products...']",
|
||||||
emptyProductRow: "[data-test-id='skeleton']",
|
emptyProductRow: "[data-test-id='skeleton']",
|
||||||
|
|
|
@ -16,5 +16,5 @@ export const BUTTON_SELECTORS = {
|
||||||
button: "button",
|
button: "button",
|
||||||
deleteAssignedItemsConsentCheckbox: '[name="delete-assigned-items-consent"]',
|
deleteAssignedItemsConsentCheckbox: '[name="delete-assigned-items-consent"]',
|
||||||
deleteSelectedElementsButton:
|
deleteSelectedElementsButton:
|
||||||
'[data-test-id = "delete-selected-elements-icon"]'
|
'[data-test-id = "delete-selected-elements-icon"]',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { PRODUCT_DETAILS } from "../../../../elements/catalog/products/product-details";
|
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 { AVAILABLE_CHANNELS_FORM } from "../../../../elements/channels/available-channels-form";
|
||||||
import { BUTTON_SELECTORS } from "../../../../elements/shared/button-selectors";
|
import { BUTTON_SELECTORS } from "../../../../elements/shared/button-selectors";
|
||||||
import { addMetadataField } from "../metadataComponent";
|
import { addMetadataField } from "../metadataComponent";
|
||||||
|
@ -90,6 +91,16 @@ export function fillUpProductGeneralInfo({ name, description, rating }) {
|
||||||
.clearAndType(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({
|
export function fillUpProductOrganization({
|
||||||
productType,
|
productType,
|
||||||
category,
|
category,
|
||||||
|
|
|
@ -250,6 +250,10 @@
|
||||||
"context": "OrderCustomer Fulfillment from Local Warehouse",
|
"context": "OrderCustomer Fulfillment from Local Warehouse",
|
||||||
"string": "Fulfill from Local Stock"
|
"string": "Fulfill from Local Stock"
|
||||||
},
|
},
|
||||||
|
"/yJJvI": {
|
||||||
|
"context": "dialog header",
|
||||||
|
"string": "Select a product type"
|
||||||
|
},
|
||||||
"/z9uo1": {
|
"/z9uo1": {
|
||||||
"context": "order returned success message",
|
"context": "order returned success message",
|
||||||
"string": "Successfully returned products!"
|
"string": "Successfully returned products!"
|
||||||
|
@ -7451,6 +7455,10 @@
|
||||||
"context": "card header",
|
"context": "card header",
|
||||||
"string": "Sign In"
|
"string": "Sign In"
|
||||||
},
|
},
|
||||||
|
"w+3Q3e": {
|
||||||
|
"context": "input label",
|
||||||
|
"string": "Product type"
|
||||||
|
},
|
||||||
"w+5Djm": {
|
"w+5Djm": {
|
||||||
"string": "Min. Order Weight"
|
"string": "Min. Order Weight"
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { Autocomplete, ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { messages } from "./messages";
|
||||||
|
|
||||||
export interface ChannelPickerDialogProps {
|
export interface ChannelPickerDialogProps {
|
||||||
channelsChoices: Array<Choice<string, string>>;
|
channelsChoices: Array<Choice<string, string>>;
|
||||||
confirmButtonState: ConfirmButtonTransitionState;
|
confirmButtonState: ConfirmButtonTransitionState;
|
||||||
|
@ -44,20 +46,12 @@ const ChannelPickerDialog: React.FC<ChannelPickerDialogProps> = ({
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onConfirm={() => onConfirm(choice)}
|
onConfirm={() => onConfirm(choice)}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage(messages.selectChannel)}
|
||||||
id: "G/pgG3",
|
|
||||||
defaultMessage: "Select a channel",
|
|
||||||
description: "dialog header",
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
choices={result}
|
choices={result}
|
||||||
fullWidth
|
fullWidth
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage(messages.channelName)}
|
||||||
defaultMessage: "Channel name",
|
|
||||||
id: "nKwgxY",
|
|
||||||
description: "select label",
|
|
||||||
})}
|
|
||||||
data-test-id="channel-autocomplete"
|
data-test-id="channel-autocomplete"
|
||||||
value={choice}
|
value={choice}
|
||||||
onChange={e => setChoice(e.target.value)}
|
onChange={e => setChoice(e.target.value)}
|
||||||
|
|
14
src/channels/components/ChannelPickerDialog/messages.ts
Normal file
14
src/channels/components/ChannelPickerDialog/messages.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
|
@ -104,7 +104,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
||||||
categories: categoryChoiceList,
|
categories: categoryChoiceList,
|
||||||
collections: collectionChoiceList,
|
collections: collectionChoiceList,
|
||||||
attributeValues,
|
attributeValues,
|
||||||
errors,
|
errors: apiErrors,
|
||||||
fetchCategories,
|
fetchCategories,
|
||||||
fetchCollections,
|
fetchCollections,
|
||||||
fetchMoreCategories,
|
fetchMoreCategories,
|
||||||
|
@ -210,6 +210,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
||||||
change,
|
change,
|
||||||
data,
|
data,
|
||||||
formErrors,
|
formErrors,
|
||||||
|
validationErrors,
|
||||||
handlers,
|
handlers,
|
||||||
submit,
|
submit,
|
||||||
isSaveDisabled,
|
isSaveDisabled,
|
||||||
|
@ -218,6 +219,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
||||||
// Comparing explicitly to false because `hasVariants` can be undefined
|
// Comparing explicitly to false because `hasVariants` can be undefined
|
||||||
const isSimpleProduct = data.productType?.hasVariants === false;
|
const isSimpleProduct = data.productType?.hasVariants === false;
|
||||||
|
|
||||||
|
const errors = [...apiErrors, ...validationErrors];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Backlink href={productListUrl()}>
|
<Backlink href={productListUrl()}>
|
||||||
|
@ -315,7 +318,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
||||||
collections={collections}
|
collections={collections}
|
||||||
data={data}
|
data={data}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
errors={errors}
|
errors={[...errors, ...channelsErrors]}
|
||||||
fetchCategories={fetchCategories}
|
fetchCategories={fetchCategories}
|
||||||
fetchCollections={fetchCollections}
|
fetchCollections={fetchCollections}
|
||||||
fetchMoreCategories={fetchMoreCategories}
|
fetchMoreCategories={fetchMoreCategories}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { MetadataFormData } from "@saleor/components/Metadata";
|
||||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||||
import {
|
import {
|
||||||
|
ProductErrorWithAttributesFragment,
|
||||||
ProductTypeQuery,
|
ProductTypeQuery,
|
||||||
SearchPagesQuery,
|
SearchPagesQuery,
|
||||||
SearchProductsQuery,
|
SearchProductsQuery,
|
||||||
|
@ -55,6 +56,7 @@ import {
|
||||||
import {
|
import {
|
||||||
validateCostPrice,
|
validateCostPrice,
|
||||||
validatePrice,
|
validatePrice,
|
||||||
|
validateProductCreateData,
|
||||||
} from "@saleor/products/utils/validation";
|
} from "@saleor/products/utils/validation";
|
||||||
import { PRODUCT_CREATE_FORM_ID } from "@saleor/products/views/ProductCreate/consts";
|
import { PRODUCT_CREATE_FORM_ID } from "@saleor/products/views/ProductCreate/consts";
|
||||||
import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
|
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 { RichTextContext } from "@saleor/utils/richText/context";
|
||||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||||
import useRichText from "@saleor/utils/richText/useRichText";
|
import useRichText from "@saleor/utils/richText/useRichText";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { createPreorderEndDateChangeHandler } from "../../utils/handlers";
|
import { createPreorderEndDateChangeHandler } from "../../utils/handlers";
|
||||||
|
@ -138,6 +140,7 @@ export interface UseProductCreateFormOutput
|
||||||
RichTextProps {
|
RichTextProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
formErrors: FormErrors<ProductCreateData>;
|
formErrors: FormErrors<ProductCreateData>;
|
||||||
|
validationErrors: ProductErrorWithAttributesFragment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseProductCreateFormRenderProps = Omit<
|
export type UseProductCreateFormRenderProps = Omit<
|
||||||
|
@ -185,6 +188,9 @@ function useProductCreateForm(
|
||||||
opts: UseProductCreateFormOpts,
|
opts: UseProductCreateFormOpts,
|
||||||
): UseProductCreateFormOutput {
|
): UseProductCreateFormOutput {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [validationErrors, setValidationErrors] = useState<
|
||||||
|
ProductErrorWithAttributesFragment[]
|
||||||
|
>([]);
|
||||||
const defaultInitialFormData: ProductCreateFormData &
|
const defaultInitialFormData: ProductCreateFormData &
|
||||||
Record<"productType", string> = {
|
Record<"productType", string> = {
|
||||||
category: "",
|
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({
|
const handleFormSubmit = useHandleFormSubmit({
|
||||||
formId,
|
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,
|
formId: PRODUCT_CREATE_FORM_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -415,14 +446,20 @@ function useProductCreateForm(
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSaveDisabled = loading || !onSubmit || !isValid();
|
const isSaveDisabled = loading || !onSubmit;
|
||||||
setIsSubmitDisabled(isSaveDisabled);
|
const isSubmitDisabled = isSaveDisabled || !isValid();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSubmitDisabled(isSubmitDisabled);
|
||||||
|
setIsDirty(true);
|
||||||
|
}, [isSubmitDisabled]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
change: handleChange,
|
change: handleChange,
|
||||||
data,
|
data,
|
||||||
disabled: isSaveDisabled,
|
disabled: isSaveDisabled,
|
||||||
formErrors: form.errors,
|
formErrors: form.errors,
|
||||||
|
validationErrors,
|
||||||
handlers: {
|
handlers: {
|
||||||
addStock: handleStockAdd,
|
addStock: handleStockAdd,
|
||||||
changeChannelPrice: handleChannelPriceChange,
|
changeChannelPrice: handleChannelPriceChange,
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { productAddUrl, ProductListUrlSortField } from "../../urls";
|
import { ProductListUrlSortField } from "../../urls";
|
||||||
import ProductList from "../ProductList";
|
import ProductList from "../ProductList";
|
||||||
import { columnsMessages } from "../ProductList/messages";
|
import { columnsMessages } from "../ProductList/messages";
|
||||||
import {
|
import {
|
||||||
|
@ -62,6 +62,7 @@ export interface ProductListPageProps
|
||||||
limits: RefreshLimitsQuery["shop"]["limits"];
|
limits: RefreshLimitsQuery["shop"]["limits"];
|
||||||
totalGridAttributes: number;
|
totalGridAttributes: number;
|
||||||
products: RelayToFlat<ProductListQuery["products"]>;
|
products: RelayToFlat<ProductListQuery["products"]>;
|
||||||
|
onAdd: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onColumnQueryChange: (query: string) => void;
|
onColumnQueryChange: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -101,6 +102,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
settings,
|
settings,
|
||||||
tabs,
|
tabs,
|
||||||
totalGridAttributes,
|
totalGridAttributes,
|
||||||
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
onColumnQueryChange,
|
onColumnQueryChange,
|
||||||
onExport,
|
onExport,
|
||||||
|
@ -229,7 +231,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
options={extensionCreateButtonItems}
|
options={extensionCreateButtonItems}
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
disabled={limitReached}
|
disabled={limitReached}
|
||||||
href={productAddUrl()}
|
onClick={onAdd}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="JFmOfi"
|
id="JFmOfi"
|
||||||
|
|
|
@ -10,7 +10,11 @@ import MultiAutocompleteSelectField, {
|
||||||
import SingleAutocompleteSelectField, {
|
import SingleAutocompleteSelectField, {
|
||||||
SingleAutocompleteChoiceType,
|
SingleAutocompleteChoiceType,
|
||||||
} from "@saleor/components/SingleAutocompleteSelectField";
|
} from "@saleor/components/SingleAutocompleteSelectField";
|
||||||
import { ProductErrorFragment } from "@saleor/graphql";
|
import {
|
||||||
|
ProductChannelListingErrorFragment,
|
||||||
|
ProductErrorCode,
|
||||||
|
ProductErrorFragment,
|
||||||
|
} from "@saleor/graphql";
|
||||||
import { ChangeEvent } from "@saleor/hooks/useForm";
|
import { ChangeEvent } from "@saleor/hooks/useForm";
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
@ -55,7 +59,7 @@ interface ProductOrganizationProps {
|
||||||
productType?: ProductType;
|
productType?: ProductType;
|
||||||
};
|
};
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
errors: ProductErrorFragment[];
|
errors: Array<ProductErrorFragment | ProductChannelListingErrorFragment>;
|
||||||
productType?: ProductType;
|
productType?: ProductType;
|
||||||
productTypeInputDisplayValue?: string;
|
productTypeInputDisplayValue?: string;
|
||||||
productTypes?: SingleAutocompleteChoiceType[];
|
productTypes?: SingleAutocompleteChoiceType[];
|
||||||
|
@ -98,9 +102,13 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const formErrors = getFormErrors(
|
const formErrors = getFormErrors(
|
||||||
["productType", "category", "collections"],
|
["productType", "category", "collections", "isPublished"],
|
||||||
errors,
|
errors,
|
||||||
);
|
);
|
||||||
|
const noCategoryError =
|
||||||
|
formErrors.isPublished?.code === ProductErrorCode.PRODUCT_WITHOUT_CATEGORY
|
||||||
|
? formErrors.isPublished
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={classes.card}>
|
<Card className={classes.card}>
|
||||||
|
@ -163,8 +171,11 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
||||||
<FormSpacer />
|
<FormSpacer />
|
||||||
<SingleAutocompleteSelectField
|
<SingleAutocompleteSelectField
|
||||||
displayValue={categoryInputDisplayValue}
|
displayValue={categoryInputDisplayValue}
|
||||||
error={!!formErrors.category}
|
error={!!(formErrors.category || noCategoryError)}
|
||||||
helperText={getProductErrorMessage(formErrors.category, intl)}
|
helperText={getProductErrorMessage(
|
||||||
|
formErrors.category || noCategoryError,
|
||||||
|
intl,
|
||||||
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: "ccXLVi",
|
id: "ccXLVi",
|
||||||
|
|
|
@ -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} />);
|
|
@ -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;
|
2
src/products/components/ProductTypePickerDialog/index.ts
Normal file
2
src/products/components/ProductTypePickerDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./ProductTypePickerDialog";
|
||||||
|
export { default } from "./ProductTypePickerDialog";
|
14
src/products/components/ProductTypePickerDialog/messages.ts
Normal file
14
src/products/components/ProductTypePickerDialog/messages.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
|
@ -462,7 +462,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
collectionsInputDisplayValue={selectedCollections}
|
collectionsInputDisplayValue={selectedCollections}
|
||||||
data={data}
|
data={data}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
errors={errors}
|
errors={[...errors, ...channelsErrors]}
|
||||||
fetchCategories={fetchCategories}
|
fetchCategories={fetchCategories}
|
||||||
fetchCollections={fetchCollections}
|
fetchCollections={fetchCollections}
|
||||||
fetchMoreCategories={fetchMoreCategories}
|
fetchMoreCategories={fetchMoreCategories}
|
||||||
|
|
|
@ -469,11 +469,12 @@ function useProductUpdateForm(
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSaveDisabled = disabled || !isValid();
|
const isSaveDisabled = disabled;
|
||||||
|
const isSubmitDisabled = isSaveDisabled || !isValid();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsSubmitDisabled(isSaveDisabled);
|
setIsSubmitDisabled(isSubmitDisabled);
|
||||||
}, [isSaveDisabled]);
|
}, [isSubmitDisabled]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
change: handleChange,
|
change: handleChange,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ProductVariantFragment,
|
ProductVariantFragment,
|
||||||
WeightUnitsEnum,
|
WeightUnitsEnum,
|
||||||
} from "@saleor/graphql";
|
} from "@saleor/graphql";
|
||||||
|
import { ProductType } from "@saleor/sdk/dist/apollo/types";
|
||||||
import { RelayToFlat } from "@saleor/types";
|
import { RelayToFlat } from "@saleor/types";
|
||||||
import { warehouseList } from "@saleor/warehouses/fixtures";
|
import { warehouseList } from "@saleor/warehouses/fixtures";
|
||||||
|
|
||||||
|
@ -3531,3 +3532,24 @@ export const variantProductImages = (placeholderImage: string) =>
|
||||||
variant(placeholderImage).product.media;
|
variant(placeholderImage).product.media;
|
||||||
export const variantSiblings = (placeholderImage: string) =>
|
export const variantSiblings = (placeholderImage: string) =>
|
||||||
variant(placeholderImage).product.variants;
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -23,7 +23,11 @@ export const productAddUrl = (params?: ProductCreateUrlQueryParams) =>
|
||||||
productAddPath + "?" + stringifyQs(params);
|
productAddPath + "?" + stringifyQs(params);
|
||||||
|
|
||||||
export const productListPath = productSection;
|
export const productListPath = productSection;
|
||||||
export type ProductListUrlDialog = "delete" | "export" | TabActionDialog;
|
export type ProductListUrlDialog =
|
||||||
|
| "delete"
|
||||||
|
| "export"
|
||||||
|
| "create-product"
|
||||||
|
| TabActionDialog;
|
||||||
export enum ProductListUrlFiltersEnum {
|
export enum ProductListUrlFiltersEnum {
|
||||||
priceFrom = "priceFrom",
|
priceFrom = "priceFrom",
|
||||||
priceTo = "priceTo",
|
priceTo = "priceTo",
|
||||||
|
@ -82,8 +86,12 @@ export type ProductUrlQueryParams = BulkAction &
|
||||||
Dialog<ProductUrlDialog> &
|
Dialog<ProductUrlDialog> &
|
||||||
SingleAction;
|
SingleAction;
|
||||||
export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction;
|
export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction;
|
||||||
|
export interface ProductCreateUrlProductType {
|
||||||
|
"product-type-id"?: string;
|
||||||
|
}
|
||||||
export type ProductCreateUrlQueryParams = Dialog<ProductCreateUrlDialog> &
|
export type ProductCreateUrlQueryParams = Dialog<ProductCreateUrlDialog> &
|
||||||
SingleAction;
|
SingleAction &
|
||||||
|
ProductCreateUrlProductType;
|
||||||
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
|
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
|
||||||
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
|
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,36 @@
|
||||||
|
import {
|
||||||
|
ProductErrorCode,
|
||||||
|
ProductErrorWithAttributesFragment,
|
||||||
|
} from "@saleor/graphql";
|
||||||
|
|
||||||
|
import { ProductCreateData } from "../components/ProductCreatePage";
|
||||||
|
|
||||||
export const validatePrice = (price: string) =>
|
export const validatePrice = (price: string) =>
|
||||||
price === "" || parseInt(price, 10) < 0;
|
price === "" || parseInt(price, 10) < 0;
|
||||||
|
|
||||||
export const validateCostPrice = (price: string) =>
|
export const validateCostPrice = (price: string) =>
|
||||||
price !== "" && parseInt(price, 10) < 0;
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
VALUES_PAGINATE_BY,
|
VALUES_PAGINATE_BY,
|
||||||
} from "@saleor/config";
|
} from "@saleor/config";
|
||||||
import {
|
import {
|
||||||
|
ProductChannelListingErrorFragment,
|
||||||
|
ProductErrorWithAttributesFragment,
|
||||||
useFileUploadMutation,
|
useFileUploadMutation,
|
||||||
useProductChannelListingUpdateMutation,
|
useProductChannelListingUpdateMutation,
|
||||||
useProductCreateMutation,
|
useProductCreateMutation,
|
||||||
|
@ -24,6 +26,7 @@ import useChannels from "@saleor/hooks/useChannels";
|
||||||
import useNavigator from "@saleor/hooks/useNavigator";
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
import useNotifier from "@saleor/hooks/useNotifier";
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
import useShop from "@saleor/hooks/useShop";
|
import useShop from "@saleor/hooks/useShop";
|
||||||
|
import { getMutationErrors } from "@saleor/misc";
|
||||||
import ProductCreatePage, {
|
import ProductCreatePage, {
|
||||||
ProductCreateData,
|
ProductCreateData,
|
||||||
} from "@saleor/products/components/ProductCreatePage";
|
} from "@saleor/products/components/ProductCreatePage";
|
||||||
|
@ -62,9 +65,15 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
|
||||||
const [productCreateComplete, setProductCreateComplete] = React.useState(
|
const [productCreateComplete, setProductCreateComplete] = React.useState(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const [selectedProductTypeId, setSelectedProductTypeId] = React.useState<
|
const selectedProductTypeId = params["product-type-id"];
|
||||||
string
|
|
||||||
>();
|
const handleSelectProductType = (productTypeId: string) =>
|
||||||
|
navigate(
|
||||||
|
productAddUrl({
|
||||||
|
...params,
|
||||||
|
"product-type-id": productTypeId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
ProductCreateUrlDialog,
|
ProductCreateUrlDialog,
|
||||||
|
@ -282,6 +291,15 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
|
||||||
updateChannelsOpts.loading ||
|
updateChannelsOpts.loading ||
|
||||||
updateVariantChannelsOpts.loading;
|
updateVariantChannelsOpts.loading;
|
||||||
|
|
||||||
|
const channelsErrors = [
|
||||||
|
...getMutationErrors(updateVariantChannelsOpts),
|
||||||
|
...getMutationErrors(updateChannelsOpts),
|
||||||
|
] as ProductChannelListingErrorFragment[];
|
||||||
|
const errors = [
|
||||||
|
...getMutationErrors(productCreateOpts),
|
||||||
|
...getMutationErrors(productVariantCreateOpts),
|
||||||
|
] as ProductErrorWithAttributesFragment[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WindowTitle
|
<WindowTitle
|
||||||
|
@ -318,14 +336,8 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
channelsErrors={
|
channelsErrors={channelsErrors}
|
||||||
updateVariantChannelsOpts.data?.productVariantChannelListingUpdate
|
errors={errors}
|
||||||
?.errors
|
|
||||||
}
|
|
||||||
errors={[
|
|
||||||
...(productCreateOpts.data?.productCreate.errors || []),
|
|
||||||
...(productVariantCreateOpts.data?.productVariantCreate.errors || []),
|
|
||||||
]}
|
|
||||||
fetchCategories={searchCategory}
|
fetchCategories={searchCategory}
|
||||||
fetchCollections={searchCollection}
|
fetchCollections={searchCollection}
|
||||||
fetchProductTypes={searchProductTypes}
|
fetchProductTypes={searchProductTypes}
|
||||||
|
@ -362,7 +374,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
|
||||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||||
onCloseDialog={() => navigate(productAddUrl())}
|
onCloseDialog={() => navigate(productAddUrl())}
|
||||||
selectedProductType={selectedProductType?.productType}
|
selectedProductType={selectedProductType?.productType}
|
||||||
onSelectProductType={id => setSelectedProductTypeId(id)}
|
onSelectProductType={handleSelectProductType}
|
||||||
onAttributeSelectBlur={searchAttributeReset}
|
onAttributeSelectBlur={searchAttributeReset}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -45,7 +45,9 @@ import {
|
||||||
getAttributeIdFromColumnValue,
|
getAttributeIdFromColumnValue,
|
||||||
isAttributeColumnValue,
|
isAttributeColumnValue,
|
||||||
} from "@saleor/products/components/ProductListPage/utils";
|
} from "@saleor/products/components/ProductListPage/utils";
|
||||||
|
import ProductTypePickerDialog from "@saleor/products/components/ProductTypePickerDialog";
|
||||||
import {
|
import {
|
||||||
|
productAddUrl,
|
||||||
productListUrl,
|
productListUrl,
|
||||||
ProductListUrlDialog,
|
ProductListUrlDialog,
|
||||||
ProductListUrlQueryParams,
|
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(
|
const filterOpts = getFilterOpts(
|
||||||
params,
|
params,
|
||||||
(mapEdgesToItems(initialFilterAttributes?.attributes) || []).filter(
|
(mapEdgesToItems(initialFilterAttributes?.attributes) || []).filter(
|
||||||
|
@ -393,6 +409,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
onColumnQueryChange={availableInGridAttributesOpts.search}
|
onColumnQueryChange={availableInGridAttributesOpts.search}
|
||||||
onFetchMore={availableInGridAttributesOpts.loadMore}
|
onFetchMore={availableInGridAttributesOpts.loadMore}
|
||||||
onUpdateListSettings={updateListSettings}
|
onUpdateListSettings={updateListSettings}
|
||||||
|
onAdd={() => openModal("create-product")}
|
||||||
onAll={resetFilters}
|
onAll={resetFilters}
|
||||||
toolbar={
|
toolbar={
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -499,6 +516,23 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
onSubmit={handleFilterTabDelete}
|
onSubmit={handleFilterTabDelete}
|
||||||
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
|
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>
|
</PaginatorContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -208242,20 +208242,18 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -210688,20 +210686,19 @@ exports[`Storyshots Views / Products / Product list limits reached 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="true"
|
|
||||||
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"
|
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"
|
disabled=""
|
||||||
role="button"
|
|
||||||
style="width:100%"
|
style="width:100%"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213217,20 +213214,18 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213791,20 +213786,18 @@ exports[`Storyshots Views / Products / Product list no channels 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -216237,20 +216230,18 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -216649,20 +216640,18 @@ exports[`Storyshots Views / Products / Product list no limits 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -219095,20 +219084,18 @@ exports[`Storyshots Views / Products / Product list with data 1`] = `
|
||||||
data-test-id="add-product"
|
data-test-id="add-product"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
aria-disabled="false"
|
|
||||||
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"
|
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%"
|
style="width:100%"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label-id"
|
class="MuiButton-label-id"
|
||||||
>
|
>
|
||||||
Create Product
|
Create Product
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -221439,6 +221426,12 @@ exports[`Storyshots Views / Products / Product list with data 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Views / Products / Product type dialog default 1`] = `
|
||||||
|
<div
|
||||||
|
style="padding:24px"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Views / Products / Product variant details attribute errors 1`] = `
|
exports[`Storyshots Views / Products / Product variant details attribute errors 1`] = `
|
||||||
<div
|
<div
|
||||||
style="padding:24px"
|
style="padding:24px"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
BulkProductErrorFragment,
|
BulkProductErrorFragment,
|
||||||
CollectionErrorFragment,
|
CollectionErrorFragment,
|
||||||
|
ProductChannelListingErrorFragment,
|
||||||
ProductErrorCode,
|
ProductErrorCode,
|
||||||
ProductErrorFragment,
|
ProductErrorFragment,
|
||||||
} from "@saleor/graphql";
|
} from "@saleor/graphql";
|
||||||
|
@ -74,7 +75,12 @@ const messages = defineMessages({
|
||||||
|
|
||||||
function getProductErrorMessage(
|
function getProductErrorMessage(
|
||||||
err:
|
err:
|
||||||
| Omit<ProductErrorFragment | CollectionErrorFragment, "__typename">
|
| Omit<
|
||||||
|
| ProductErrorFragment
|
||||||
|
| CollectionErrorFragment
|
||||||
|
| ProductChannelListingErrorFragment,
|
||||||
|
"__typename"
|
||||||
|
>
|
||||||
| undefined,
|
| undefined,
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
): string {
|
): string {
|
||||||
|
|
Loading…
Reference in a new issue