diff --git a/CHANGELOG.md b/CHANGELOG.md index cd2a5a5c4..c0b233d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cypress/e2e/products/createProduct.js b/cypress/e2e/products/createProduct.js index 3946bfb2d..97952df35 100644 --- a/cypress/e2e/products/createProduct.js +++ b/cypress/e2e/products/createProduct.js @@ -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); } }); diff --git a/cypress/elements/catalog/collection-selectors.js b/cypress/elements/catalog/collection-selectors.js index d7fe54aa3..8f1d4badc 100644 --- a/cypress/elements/catalog/collection-selectors.js +++ b/cypress/elements/catalog/collection-selectors.js @@ -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 => diff --git a/cypress/elements/catalog/products/products-list.js b/cypress/elements/catalog/products/products-list.js index 6fb181031..1b303210a 100644 --- a/cypress/elements/catalog/products/products-list.js +++ b/cypress/elements/catalog/products/products-list.js @@ -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']", diff --git a/cypress/elements/shared/button-selectors.js b/cypress/elements/shared/button-selectors.js index ee9b7f403..1c171d6bc 100644 --- a/cypress/elements/shared/button-selectors.js +++ b/cypress/elements/shared/button-selectors.js @@ -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"]', }; diff --git a/cypress/support/pages/catalog/products/productDetailsPage.js b/cypress/support/pages/catalog/products/productDetailsPage.js index 33dc76617..747ee2c44 100644 --- a/cypress/support/pages/catalog/products/productDetailsPage.js +++ b/cypress/support/pages/catalog/products/productDetailsPage.js @@ -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, diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 7c135754b..6466d6fc7 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" }, diff --git a/src/channels/components/ChannelPickerDialog/ChannelPickerDialog.tsx b/src/channels/components/ChannelPickerDialog/ChannelPickerDialog.tsx index 49c6b4542..a292835f3 100644 --- a/src/channels/components/ChannelPickerDialog/ChannelPickerDialog.tsx +++ b/src/channels/components/ChannelPickerDialog/ChannelPickerDialog.tsx @@ -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>; confirmButtonState: ConfirmButtonTransitionState; @@ -44,20 +46,12 @@ const ChannelPickerDialog: React.FC = ({ 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)} > setChoice(e.target.value)} diff --git a/src/channels/components/ChannelPickerDialog/messages.ts b/src/channels/components/ChannelPickerDialog/messages.ts new file mode 100644 index 000000000..82af1146e --- /dev/null +++ b/src/channels/components/ChannelPickerDialog/messages.ts @@ -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", + }, +}); diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index e2c97dd97..94eeca82a 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -104,7 +104,7 @@ export const ProductCreatePage: React.FC = ({ categories: categoryChoiceList, collections: collectionChoiceList, attributeValues, - errors, + errors: apiErrors, fetchCategories, fetchCollections, fetchMoreCategories, @@ -210,6 +210,7 @@ export const ProductCreatePage: React.FC = ({ change, data, formErrors, + validationErrors, handlers, submit, isSaveDisabled, @@ -218,6 +219,8 @@ export const ProductCreatePage: React.FC = ({ // Comparing explicitly to false because `hasVariants` can be undefined const isSimpleProduct = data.productType?.hasVariants === false; + const errors = [...apiErrors, ...validationErrors]; + return ( @@ -315,7 +318,7 @@ export const ProductCreatePage: React.FC = ({ collections={collections} data={data} disabled={loading} - errors={errors} + errors={[...errors, ...channelsErrors]} fetchCategories={fetchCategories} fetchCollections={fetchCollections} fetchMoreCategories={fetchMoreCategories} diff --git a/src/products/components/ProductCreatePage/form.tsx b/src/products/components/ProductCreatePage/form.tsx index 0601af7fb..5e0a6aed6 100644 --- a/src/products/components/ProductCreatePage/form.tsx +++ b/src/products/components/ProductCreatePage/form.tsx @@ -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; + 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, diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 69602b81e..f3fb3cc1f 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -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; + onAdd: () => void; onExport: () => void; onColumnQueryChange: (query: string) => void; } @@ -101,6 +102,7 @@ export const ProductListPage: React.FC = props => { settings, tabs, totalGridAttributes, + onAdd, onAll, onColumnQueryChange, onExport, @@ -229,7 +231,7 @@ export const ProductListPage: React.FC = props => { options={extensionCreateButtonItems} data-test-id="add-product" disabled={limitReached} - href={productAddUrl()} + onClick={onAdd} > ; productType?: ProductType; productTypeInputDisplayValue?: string; productTypes?: SingleAutocompleteChoiceType[]; @@ -98,9 +102,13 @@ const ProductOrganization: React.FC = 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 ( @@ -163,8 +171,11 @@ const ProductOrganization: React.FC = props => { 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", () => ); diff --git a/src/products/components/ProductTypePickerDialog/ProductTypePickerDialog.tsx b/src/products/components/ProductTypePickerDialog/ProductTypePickerDialog.tsx new file mode 100644 index 000000000..51dce95a2 --- /dev/null +++ b/src/products/components/ProductTypePickerDialog/ProductTypePickerDialog.tsx @@ -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 = ({ + 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 ( + onConfirm(choice)} + title={intl.formatMessage(messages.selectProductType)} + disabled={!choice} + > + setChoice(e.target.value)} + fetchChoices={fetchProductTypes} + data-test-id="dialog-product-type" + {...fetchMoreProductTypes} + /> + + ); +}; +ProductTypePickerDialog.displayName = "ProductTypePickerDialog"; +export default ProductTypePickerDialog; diff --git a/src/products/components/ProductTypePickerDialog/index.ts b/src/products/components/ProductTypePickerDialog/index.ts new file mode 100644 index 000000000..70b1bd6bc --- /dev/null +++ b/src/products/components/ProductTypePickerDialog/index.ts @@ -0,0 +1,2 @@ +export * from "./ProductTypePickerDialog"; +export { default } from "./ProductTypePickerDialog"; diff --git a/src/products/components/ProductTypePickerDialog/messages.ts b/src/products/components/ProductTypePickerDialog/messages.ts new file mode 100644 index 000000000..8f58e6e4a --- /dev/null +++ b/src/products/components/ProductTypePickerDialog/messages.ts @@ -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", + }, +}); diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 39ac7d9c9..19e1a5892 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -462,7 +462,7 @@ export const ProductUpdatePage: React.FC = ({ collectionsInputDisplayValue={selectedCollections} data={data} disabled={disabled} - errors={errors} + errors={[...errors, ...channelsErrors]} fetchCategories={fetchCategories} fetchCollections={fetchCollections} fetchMoreCategories={fetchMoreCategories} diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index c7179a2c4..8bba16271 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -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, diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index 22021f928..41d72acf7 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -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> = [ + { + hasVariants: true, + id: "UHJvZHVjdFR5cGU6Nw==", + name: "Salt", + }, + { + hasVariants: true, + id: "UHJvZHVjdFR5cGU6Nw==", + name: "Sugar", + }, + { + hasVariants: true, + id: "UHJvZHVjdFR5cGU6Nw==", + name: "Mushroom", + }, +]; diff --git a/src/products/urls.ts b/src/products/urls.ts index c7ce9d549..82e013a57 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -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 & SingleAction; export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction; +export interface ProductCreateUrlProductType { + "product-type-id"?: string; +} export type ProductCreateUrlQueryParams = Dialog & - SingleAction; + SingleAction & + ProductCreateUrlProductType; export const productUrl = (id: string, params?: ProductUrlQueryParams) => productPath(encodeURIComponent(id)) + "?" + stringifyQs(params); diff --git a/src/products/utils/validation.ts b/src/products/utils/validation.ts index ebaa9dd15..c8962acc4 100644 --- a/src/products/utils/validation.ts +++ b/src/products/utils/validation.ts @@ -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; +}; diff --git a/src/products/views/ProductCreate/ProductCreate.tsx b/src/products/views/ProductCreate/ProductCreate.tsx index 47c591c22..787f5f7b1 100644 --- a/src/products/views/ProductCreate/ProductCreate.tsx +++ b/src/products/views/ProductCreate/ProductCreate.tsx @@ -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 = ({ 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 = ({ params }) => { updateChannelsOpts.loading || updateVariantChannelsOpts.loading; + const channelsErrors = [ + ...getMutationErrors(updateVariantChannelsOpts), + ...getMutationErrors(updateChannelsOpts), + ] as ProductChannelListingErrorFragment[]; + const errors = [ + ...getMutationErrors(productCreateOpts), + ...getMutationErrors(productVariantCreateOpts), + ] as ProductErrorWithAttributesFragment[]; + return ( <> = ({ 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 = ({ params }) => { fetchMoreAttributeValues={fetchMoreAttributeValues} onCloseDialog={() => navigate(productAddUrl())} selectedProductType={selectedProductType?.productType} - onSelectProductType={id => setSelectedProductTypeId(id)} + onSelectProductType={handleSelectProductType} onAttributeSelectBlur={searchAttributeReset} /> diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index ce80142b8..487c8e7fd 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -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 = ({ 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 = ({ params }) => { onColumnQueryChange={availableInGridAttributesOpts.search} onFetchMore={availableInGridAttributesOpts.loadMore} onUpdateListSettings={updateListSettings} + onAdd={() => openModal("create-product")} onAll={resetFilters} toolbar={ = ({ params }) => { onSubmit={handleFilterTabDelete} tabName={maybe(() => tabs[currentTab - 1].name, "...")} /> + + navigate( + productAddUrl({ + "product-type-id": productTypeId, + }), + ) + } + /> ); }; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 520200683..222161e48 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -208242,20 +208242,18 @@ exports[`Storyshots Views / Products / Product list default 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -210688,20 +210686,19 @@ exports[`Storyshots Views / Products / Product list limits reached 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -213217,20 +213214,18 @@ exports[`Storyshots Views / Products / Product list loading 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -213791,20 +213786,18 @@ exports[`Storyshots Views / Products / Product list no channels 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -216237,20 +216230,18 @@ exports[`Storyshots Views / Products / Product list no data 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -216649,20 +216640,18 @@ exports[`Storyshots Views / Products / Product list no limits 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -219095,20 +219084,18 @@ exports[`Storyshots Views / Products / Product list with data 1`] = ` data-test-id="add-product" role="group" > - Create Product - + @@ -221439,6 +221426,12 @@ exports[`Storyshots Views / Products / Product list with data 1`] = ` `; +exports[`Storyshots Views / Products / Product type dialog default 1`] = ` +
+`; + exports[`Storyshots Views / Products / Product variant details attribute errors 1`] = `
+ | Omit< + | ProductErrorFragment + | CollectionErrorFragment + | ProductChannelListingErrorFragment, + "__typename" + > | undefined, intl: IntlShape, ): string {