diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a46f356..f4e55add7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,11 +42,13 @@ All notable, unreleased changes to this project will be documented in this file. - Fix return to previous page on screen size change - #710 by @orzechdev - Add variants reordering possibility - #716 by @orzechdev - Fix avatar change button - #719 by @orzechdev +- Add slug field to product, collection, category & page details (update and create) - #720 by @mmarkusik - Allow product variant to be set as default - #721 by @tomaszszymanski129 - Change plural form of "informations" to "information" strings across the app #722 by @mmarkusik - Fix misaligned rich text draft controls - #725 by @orzechdev - Allow taxes to be configured per product - #728 by @dominik-zeglen + ## 2.10.1 - Add weight field and fix warehouse country selection - #597 by @dominik-zeglen diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 7a877a76e..546019980 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1827,9 +1827,6 @@ "src_dot_components_dot_SeoForm_dot_1991321627": { "string": "Search engine description" }, - "src_dot_components_dot_SeoForm_dot_2378618579": { - "string": "If empty, the preview shows what will be autogenerated." - }, "src_dot_components_dot_SeoForm_dot_3198271020": { "context": "button", "string": "Edit website SEO" @@ -1837,10 +1834,16 @@ "src_dot_components_dot_SeoForm_dot_3468022343": { "string": "Search Engine Preview" }, + "src_dot_components_dot_SeoForm_dot_3478065224": { + "string": "Slug" + }, "src_dot_components_dot_SeoForm_dot_3877274856": { "context": "character limit", "string": "{numberOfCharacters} of {maxCharacters} characters" }, + "src_dot_components_dot_SeoForm_dot_seoFieldMessage": { + "string": "If empty, the preview shows what will be autogenerated." + }, "src_dot_components_dot_SingleAutocompleteSelectField_dot_1477537381": { "context": "add custom select input option", "string": "Add new value: {value}" @@ -3567,16 +3570,6 @@ "context": "page status", "string": "Not Published" }, - "src_dot_pages_dot_components_dot_PageSlug_dot_1324178587": { - "string": "URL" - }, - "src_dot_pages_dot_components_dot_PageSlug_dot_3478065224": { - "context": "page internal name", - "string": "Slug" - }, - "src_dot_pages_dot_components_dot_PageSlug_dot_4210828158": { - "string": "If empty, URL will be autogenerated from Page Name" - }, "src_dot_pages_dot_views_dot_1068617485": { "context": "header", "string": "Create Page" @@ -5711,6 +5704,9 @@ "src_dot_utils_dot_errors_dot_misconfigured": { "string": "Plugin is misconfigured and cannot be activated" }, + "src_dot_utils_dot_errors_dot_nameAlreadyTaken": { + "string": "This name is already taken. Please provide another." + }, "src_dot_utils_dot_errors_dot_noShippingAddress": { "context": "error message", "string": "Cannot choose a shipping method for an order without the shipping address" diff --git a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx index 202abe404..0ac329841 100644 --- a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx +++ b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx @@ -19,6 +19,7 @@ import CategoryDetailsForm from "../../components/CategoryDetailsForm"; export interface FormData extends MetadataFormData { description: RawDraftContentState; name: string; + slug: string; seoTitle: string; seoDescription: string; } @@ -29,7 +30,8 @@ const initialData: FormData = { name: "", privateMetadata: [], seoDescription: "", - seoTitle: "" + seoTitle: "", + slug: "" }; export interface CategoryCreatePageProps { @@ -77,10 +79,13 @@ export const CategoryCreatePage: React.FC = ({ /> = ({ name: category.name || "", privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), seoDescription: category.seoDescription || "", - seoTitle: category.seoTitle || "" + seoTitle: category.seoTitle || "", + slug: category?.slug || "" } : { backgroundImageAlt: "", @@ -127,7 +129,8 @@ export const CategoryUpdatePage: React.FC = ({ name: "", privateMetadata: undefined, seoDescription: "", - seoTitle: "" + seoTitle: "", + slug: "" }; const handleSubmit = (data: FormData) => { @@ -175,10 +178,13 @@ export const CategoryUpdatePage: React.FC = ({ defaultMessage: "Add search engine title and description to make this category easier to find" })} + errors={errors} title={data.seoTitle} titlePlaceholder={data.name} description={data.seoDescription} descriptionPlaceholder={data.name} + slug={data.slug} + slugPlaceholder={data.name} loading={!category} onChange={change} disabled={disabled} diff --git a/src/categories/fixtures.ts b/src/categories/fixtures.ts index d51e93b93..ddc277a83 100644 --- a/src/categories/fixtures.ts +++ b/src/categories/fixtures.ts @@ -511,7 +511,8 @@ export const category: ( } }, seoDescription: null, - seoTitle: null + seoTitle: null, + slug: "coffees" }); export const errors = [ { diff --git a/src/categories/types/CategoryCreate.ts b/src/categories/types/CategoryCreate.ts index 21991ef8d..a7f5336e7 100644 --- a/src/categories/types/CategoryCreate.ts +++ b/src/categories/types/CategoryCreate.ts @@ -38,6 +38,7 @@ export interface CategoryCreate_categoryCreate_category { privateMetadata: (CategoryCreate_categoryCreate_category_privateMetadata | null)[]; backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null; name: string; + slug: string; descriptionJson: any; seoDescription: string | null; seoTitle: string | null; diff --git a/src/categories/types/CategoryDetails.ts b/src/categories/types/CategoryDetails.ts index 5cf7baa08..d6ce553eb 100644 --- a/src/categories/types/CategoryDetails.ts +++ b/src/categories/types/CategoryDetails.ts @@ -147,6 +147,7 @@ export interface CategoryDetails_category { privateMetadata: (CategoryDetails_category_privateMetadata | null)[]; backgroundImage: CategoryDetails_category_backgroundImage | null; name: string; + slug: string; descriptionJson: any; seoDescription: string | null; seoTitle: string | null; diff --git a/src/categories/types/CategoryUpdate.ts b/src/categories/types/CategoryUpdate.ts index 1be777c04..2a7bb4429 100644 --- a/src/categories/types/CategoryUpdate.ts +++ b/src/categories/types/CategoryUpdate.ts @@ -38,6 +38,7 @@ export interface CategoryUpdate_categoryUpdate_category { privateMetadata: (CategoryUpdate_categoryUpdate_category_privateMetadata | null)[]; backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null; name: string; + slug: string; descriptionJson: any; seoDescription: string | null; seoTitle: string | null; diff --git a/src/categories/views/CategoryCreate.tsx b/src/categories/views/CategoryCreate.tsx index 606fab85b..761b12e20 100644 --- a/src/categories/views/CategoryCreate.tsx +++ b/src/categories/views/CategoryCreate.tsx @@ -52,7 +52,8 @@ export const CategoryCreateView: React.FC = ({ seo: { description: formData.seoDescription, title: formData.seoTitle - } + }, + slug: formData.slug }, parent: parentId || null } diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index cb875d01d..cd52af91c 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -186,7 +186,8 @@ export const CategoryDetails: React.FC = ({ seo: { description: formData.seoDescription, title: formData.seoTitle - } + }, + slug: formData.slug } } }); diff --git a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx index f31998883..777ef78c7 100644 --- a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx +++ b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx @@ -28,6 +28,7 @@ export interface CollectionCreatePageFormData extends MetadataFormData { backgroundImageAlt: string; description: RawDraftContentState; name: string; + slug: string; publicationDate: string; isPublished: boolean; seoDescription: string; @@ -55,7 +56,8 @@ const initialForm: CollectionCreatePageFormData = { privateMetadata: [], publicationDate: "", seoDescription: "", - seoTitle: "" + seoTitle: "", + slug: "" }; const CollectionCreatePage: React.FC = ({ @@ -133,6 +135,7 @@ const CollectionCreatePage: React.FC = ({ /> = ({ defaultMessage: "Add search engine title and description to make this collection easier to find" })} + slug={data.slug} + slugPlaceholder={data.name} title={data.seoTitle} titlePlaceholder={data.name} onChange={change} diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index 8fba3282d..3127745b3 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -33,6 +33,7 @@ export interface CollectionDetailsPageFormData extends MetadataFormData { backgroundImageAlt: string; description: RawDraftContentState; name: string; + slug: string; publicationDate: string; seoDescription: string; seoTitle: string; @@ -101,7 +102,8 @@ const CollectionDetailsPage: React.FC = ({ ), publicationDate: maybe(() => collection.publicationDate, ""), seoDescription: maybe(() => collection.seoDescription, ""), - seoTitle: maybe(() => collection.seoTitle, "") + seoTitle: maybe(() => collection.seoTitle, ""), + slug: collection?.slug || "" }} onSubmit={handleSubmit} confirmLeave @@ -149,6 +151,9 @@ const CollectionDetailsPage: React.FC = ({ defaultMessage: "Add search engine title and description to make this collection easier to find" })} + errors={errors} + slug={data.slug} + slugPlaceholder={data.name} title={data.seoTitle} titlePlaceholder={maybe(() => collection.name)} onChange={change} diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index 0119379c8..f604aa199 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -167,5 +167,6 @@ export const collection: ( }, publicationDate: "2018-08-25T18:45:54.125Z", seoDescription: "", - seoTitle: "" + seoTitle: "", + slug: "summer-collection" }); diff --git a/src/collections/types/CollectionDetails.ts b/src/collections/types/CollectionDetails.ts index f46367d6a..1746d93fc 100644 --- a/src/collections/types/CollectionDetails.ts +++ b/src/collections/types/CollectionDetails.ts @@ -71,6 +71,7 @@ export interface CollectionDetails_collection { metadata: (CollectionDetails_collection_metadata | null)[]; privateMetadata: (CollectionDetails_collection_privateMetadata | null)[]; backgroundImage: CollectionDetails_collection_backgroundImage | null; + slug: string; descriptionJson: any; publicationDate: any | null; seoDescription: string | null; diff --git a/src/collections/types/CollectionUpdate.ts b/src/collections/types/CollectionUpdate.ts index a3b85081f..f390dd7be 100644 --- a/src/collections/types/CollectionUpdate.ts +++ b/src/collections/types/CollectionUpdate.ts @@ -34,6 +34,7 @@ export interface CollectionUpdate_collectionUpdate_collection { metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[]; privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[]; backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null; + slug: string; descriptionJson: any; publicationDate: any | null; seoDescription: string | null; diff --git a/src/collections/types/CollectionUpdateWithHomepage.ts b/src/collections/types/CollectionUpdateWithHomepage.ts index d034e7cfe..7c982190f 100644 --- a/src/collections/types/CollectionUpdateWithHomepage.ts +++ b/src/collections/types/CollectionUpdateWithHomepage.ts @@ -56,6 +56,7 @@ export interface CollectionUpdateWithHomepage_collectionUpdate_collection { metadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_metadata | null)[]; privateMetadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata | null)[]; backgroundImage: CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage | null; + slug: string; descriptionJson: any; publicationDate: any | null; seoDescription: string | null; diff --git a/src/collections/types/CreateCollection.ts b/src/collections/types/CreateCollection.ts index 7f1646436..efb28ca4b 100644 --- a/src/collections/types/CreateCollection.ts +++ b/src/collections/types/CreateCollection.ts @@ -34,6 +34,7 @@ export interface CreateCollection_collectionCreate_collection { metadata: (CreateCollection_collectionCreate_collection_metadata | null)[]; privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[]; backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null; + slug: string; descriptionJson: any; publicationDate: any | null; seoDescription: string | null; diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 968238155..9ca6d0798 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -180,7 +180,8 @@ export const CollectionDetails: React.FC = ({ seo: { description: formData.seoDescription, title: formData.seoTitle - } + }, + slug: formData.slug }; const isFeatured = data.shop.homepageCollection ? data.shop.homepageCollection.id === data.collection.id diff --git a/src/components/SeoForm/SeoForm.tsx b/src/components/SeoForm/SeoForm.tsx index d2d753825..6af608b88 100644 --- a/src/components/SeoForm/SeoForm.tsx +++ b/src/components/SeoForm/SeoForm.tsx @@ -4,13 +4,26 @@ import CardContent from "@material-ui/core/CardContent"; import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; +import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; +import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; +import { getFieldError, getProductErrorMessage } from "@saleor/utils/errors"; +import getPageErrorMessage from "@saleor/utils/errors/page"; import classNames from "classnames"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { defineMessages, FormattedMessage, useIntl } from "react-intl"; +import slugify from "slugify"; import CardTitle from "../CardTitle"; import FormSpacer from "../FormSpacer"; +enum SeoField { + slug = "slug", + title = "seoTitle", + description = "seoDescription" +} + +const SLUG_REGEX = /^[a-zA-Z0-9\-\_]+$/; + const useStyles = makeStyles( theme => ({ addressBar: { @@ -65,9 +78,13 @@ interface SeoFormProps { description?: string; descriptionPlaceholder: string; disabled?: boolean; + errors?: Array; loading?: boolean; helperText?: string; + allowEmptySlug?: boolean; title: string; + slug: string; + slugPlaceholder?: string; titlePlaceholder: string; onChange(event: any); onClick?(); @@ -78,18 +95,61 @@ const SeoForm: React.FC = props => { description, descriptionPlaceholder, disabled, + errors = [], helperText, + allowEmptySlug = false, loading, title, + slug, + slugPlaceholder, titlePlaceholder, onChange } = props; const classes = useStyles(props); const intl = useIntl(); + const [expanded, setExpansionStatus] = React.useState(false); + const toggleExpansion = () => setExpansionStatus(!expanded); + const shouldDisplayHelperText = helperText && !expanded; + + const { seoFieldMessage } = defineMessages({ + seoFieldMessage: { + defaultMessage: "If empty, the preview shows what will be autogenerated." + } + }); + + const getSlugHelperMessage = () => { + const error = !!getError(SeoField.slug); + + if (allowEmptySlug && !error) { + return intl.formatMessage(seoFieldMessage); + } + + return error ? getSlugErrorMessage() : ""; + }; + + const getSlugErrorMessage = () => { + const error = getError(SeoField.slug); + const { __typename: type } = error; + + return type === "ProductError" + ? getProductErrorMessage(error as ProductErrorFragment, intl) + : getPageErrorMessage(error as PageErrorFragment, intl); + }; + + const handleSlugChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + if (value === "" || SLUG_REGEX.test(value)) { + onChange(event); + } + }; + + const getError = (fieldName: SeoField) => getFieldError(errors, fieldName); + return ( = props => { } /> - {helperText && ( + {shouldDisplayHelperText && ( @@ -116,13 +176,43 @@ const SeoForm: React.FC = props => { {expanded && (
+
+ +
+ {slug?.length > 0 && ( + + + + )} +
+ } + helperText={getSlugHelperMessage()} + value={slug?.slice(0, 69)} + disabled={loading || disabled} + placeholder={slug || slugify(slugPlaceholder, { lower: true })} + onChange={handleSlugChange} + fullWidth + /> + +
- {title.length > 0 && ( + {title?.length > 0 && ( = props => { )} } - helperText={intl.formatMessage({ - defaultMessage: - "If empty, the preview shows what will be autogenerated." - })} - value={title.slice(0, 69)} + helperText={intl.formatMessage(seoFieldMessage)} + value={title?.slice(0, 69)} disabled={loading || disabled} placeholder={titlePlaceholder} onChange={onChange} @@ -148,13 +235,13 @@ const SeoForm: React.FC = props => { />
- {description.length > 0 && ( + {description?.length > 0 && ( = props => { )} } - helperText={intl.formatMessage({ - defaultMessage: - "If empty, the preview shows what will be autogenerated." - })} - value={description ? description.slice(0, 299) : undefined} + helperText={intl.formatMessage(seoFieldMessage)} + value={description?.slice(0, 299)} onChange={onChange} disabled={loading || disabled} fullWidth diff --git a/src/fragments/categories.ts b/src/fragments/categories.ts index 3eebe58d9..5545d6220 100644 --- a/src/fragments/categories.ts +++ b/src/fragments/categories.ts @@ -24,6 +24,7 @@ export const categoryDetailsFragment = gql` url } name + slug descriptionJson seoDescription seoTitle diff --git a/src/fragments/collections.ts b/src/fragments/collections.ts index a27129ce3..984db729b 100644 --- a/src/fragments/collections.ts +++ b/src/fragments/collections.ts @@ -20,6 +20,7 @@ export const collectionDetailsFragment = gql` alt url } + slug descriptionJson publicationDate seoDescription diff --git a/src/fragments/products.ts b/src/fragments/products.ts index fb2ece335..71ecbccfd 100644 --- a/src/fragments/products.ts +++ b/src/fragments/products.ts @@ -113,6 +113,7 @@ export const productFragmentDetails = gql` ...ProductVariantAttributesFragment ...MetadataFragment name + slug descriptionJson seoTitle seoDescription diff --git a/src/fragments/types/CategoryDetailsFragment.ts b/src/fragments/types/CategoryDetailsFragment.ts index 49e5640ea..e1b2d5fbb 100644 --- a/src/fragments/types/CategoryDetailsFragment.ts +++ b/src/fragments/types/CategoryDetailsFragment.ts @@ -36,6 +36,7 @@ export interface CategoryDetailsFragment { privateMetadata: (CategoryDetailsFragment_privateMetadata | null)[]; backgroundImage: CategoryDetailsFragment_backgroundImage | null; name: string; + slug: string; descriptionJson: any; seoDescription: string | null; seoTitle: string | null; diff --git a/src/fragments/types/CollectionDetailsFragment.ts b/src/fragments/types/CollectionDetailsFragment.ts index d2dc154a8..0464eb6e7 100644 --- a/src/fragments/types/CollectionDetailsFragment.ts +++ b/src/fragments/types/CollectionDetailsFragment.ts @@ -32,6 +32,7 @@ export interface CollectionDetailsFragment { metadata: (CollectionDetailsFragment_metadata | null)[]; privateMetadata: (CollectionDetailsFragment_privateMetadata | null)[]; backgroundImage: CollectionDetailsFragment_backgroundImage | null; + slug: string; descriptionJson: any; publicationDate: any | null; seoDescription: string | null; diff --git a/src/fragments/types/Product.ts b/src/fragments/types/Product.ts index c824b1e72..203e734a9 100644 --- a/src/fragments/types/Product.ts +++ b/src/fragments/types/Product.ts @@ -213,6 +213,7 @@ export interface Product { metadata: (Product_metadata | null)[]; privateMetadata: (Product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index e35841070..772049b04 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -23,7 +23,6 @@ import { useIntl } from "react-intl"; import { maybe } from "../../../misc"; import { PageDetails_page } from "../../types/PageDetails"; import PageInfo from "../PageInfo"; -import PageSlug from "../PageSlug"; export interface FormData { content: RawDraftContentState; @@ -39,6 +38,7 @@ export interface PageDetailsPageProps { disabled: boolean; errors: PageErrorFragment[]; page: PageDetails_page; + allowEmptySlug?: boolean; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; onRemove: () => void; @@ -56,6 +56,7 @@ const PageDetailsPage: React.FC = ({ }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); + const pageExists = page !== null; const initialForm: FormData = { content: maybe( @@ -78,7 +79,7 @@ const PageDetailsPage: React.FC = ({ = ({ /> = ({ "" )} onChange={change} + slug={data.slug} + slugPlaceholder={data.title} title={data.seoTitle} titlePlaceholder={data.title} helperText={intl.formatMessage({ @@ -116,12 +121,6 @@ const PageDetailsPage: React.FC = ({ />
- ) => void; -} - -const PageSlug: React.FC = ({ - data, - disabled, - errors, - onChange -}) => { - const intl = useIntl(); - - const formErrors = getFormErrors(["slug"], errors); - - return ( - - - - - - - ); -}; -PageSlug.displayName = "PageSlug"; -export default PageSlug; diff --git a/src/pages/components/PageSlug/index.ts b/src/pages/components/PageSlug/index.ts deleted file mode 100644 index d307cb899..000000000 --- a/src/pages/components/PageSlug/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./PageSlug"; -export * from "./PageSlug"; diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index 7435200c0..031abec20 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -61,6 +61,7 @@ interface FormData extends MetadataFormData { isAvailableForPurchase: boolean; isPublished: boolean; name: string; + slug: string; productType: string; publicationDate: string; seoDescription: string; @@ -169,6 +170,7 @@ export const ProductCreatePage: React.FC = ({ seoDescription: "", seoTitle: "", sku: null, + slug: "", stockQuantity: null, taxCode: null, trackInventory: false, @@ -323,11 +325,14 @@ export const ProductCreatePage: React.FC = ({ )} = ({ )} = ({ .getPlainText() .slice(0, 300) )} + slug={data.slug} + slugPlaceholder={data.name} loading={disabled} onClick={onSeoClick} onChange={change} diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index e198a5dd5..1207f656c 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -257,6 +257,7 @@ export const product: ( seoDescription: "Seo description", seoTitle: "Seo title", sku: "59661-34207", + slug: "Borders", taxType: { __typename: "TaxType", description: "standard", diff --git a/src/products/types/ProductCreate.ts b/src/products/types/ProductCreate.ts index 6411a0ac5..f31768995 100644 --- a/src/products/types/ProductCreate.ts +++ b/src/products/types/ProductCreate.ts @@ -219,6 +219,7 @@ export interface ProductCreate_productCreate_product { metadata: (ProductCreate_productCreate_product_metadata | null)[]; privateMetadata: (ProductCreate_productCreate_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index cc2aebf50..21a614bc0 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -213,6 +213,7 @@ export interface ProductDetails_product { metadata: (ProductDetails_product_metadata | null)[]; privateMetadata: (ProductDetails_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductImageCreate.ts b/src/products/types/ProductImageCreate.ts index e401cd608..c1782b45b 100644 --- a/src/products/types/ProductImageCreate.ts +++ b/src/products/types/ProductImageCreate.ts @@ -219,6 +219,7 @@ export interface ProductImageCreate_productImageCreate_product { metadata: (ProductImageCreate_productImageCreate_product_metadata | null)[]; privateMetadata: (ProductImageCreate_productImageCreate_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductImageUpdate.ts b/src/products/types/ProductImageUpdate.ts index eb193105b..2df7a1953 100644 --- a/src/products/types/ProductImageUpdate.ts +++ b/src/products/types/ProductImageUpdate.ts @@ -219,6 +219,7 @@ export interface ProductImageUpdate_productImageUpdate_product { metadata: (ProductImageUpdate_productImageUpdate_product_metadata | null)[]; privateMetadata: (ProductImageUpdate_productImageUpdate_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductUpdate.ts b/src/products/types/ProductUpdate.ts index c64dae20a..8da5ceab4 100644 --- a/src/products/types/ProductUpdate.ts +++ b/src/products/types/ProductUpdate.ts @@ -219,6 +219,7 @@ export interface ProductUpdate_productUpdate_product { metadata: (ProductUpdate_productUpdate_product_metadata | null)[]; privateMetadata: (ProductUpdate_productUpdate_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductVariantReorder.ts b/src/products/types/ProductVariantReorder.ts index a23d7dd06..1870dd9c6 100644 --- a/src/products/types/ProductVariantReorder.ts +++ b/src/products/types/ProductVariantReorder.ts @@ -219,6 +219,7 @@ export interface ProductVariantReorder_productVariantReorder_product { metadata: (ProductVariantReorder_productVariantReorder_product_metadata | null)[]; privateMetadata: (ProductVariantReorder_productVariantReorder_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/types/ProductVariantSetDefault.ts b/src/products/types/ProductVariantSetDefault.ts index 2fb7dcb8a..2448d3574 100644 --- a/src/products/types/ProductVariantSetDefault.ts +++ b/src/products/types/ProductVariantSetDefault.ts @@ -58,12 +58,19 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product_produ values: (ProductVariantSetDefault_productVariantSetDefault_product_productType_variantAttributes_values | null)[] | null; } +export interface ProductVariantSetDefault_productVariantSetDefault_product_productType_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; +} + export interface ProductVariantSetDefault_productVariantSetDefault_product_productType { __typename: "ProductType"; id: string; variantAttributes: (ProductVariantSetDefault_productVariantSetDefault_product_productType_variantAttributes | null)[] | null; name: string; hasVariants: boolean; + taxType: ProductVariantSetDefault_productVariantSetDefault_product_productType_taxType | null; } export interface ProductVariantSetDefault_productVariantSetDefault_product_pricing_priceRangeUndiscounted_start_gross { @@ -197,6 +204,12 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product_weigh value: number; } +export interface ProductVariantSetDefault_productVariantSetDefault_product_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; +} + export interface ProductVariantSetDefault_productVariantSetDefault_product { __typename: "Product"; id: string; @@ -206,6 +219,7 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product { metadata: (ProductVariantSetDefault_productVariantSetDefault_product_metadata | null)[]; privateMetadata: (ProductVariantSetDefault_productVariantSetDefault_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; @@ -222,6 +236,7 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product { images: (ProductVariantSetDefault_productVariantSetDefault_product_images | null)[] | null; variants: (ProductVariantSetDefault_productVariantSetDefault_product_variants | null)[] | null; weight: ProductVariantSetDefault_productVariantSetDefault_product_weight | null; + taxType: ProductVariantSetDefault_productVariantSetDefault_product_taxType | null; availableForPurchase: any | null; visibleInListings: boolean; } diff --git a/src/products/types/SimpleProductUpdate.ts b/src/products/types/SimpleProductUpdate.ts index 777db7bd9..cbc11614e 100644 --- a/src/products/types/SimpleProductUpdate.ts +++ b/src/products/types/SimpleProductUpdate.ts @@ -219,6 +219,7 @@ export interface SimpleProductUpdate_productUpdate_product { metadata: (SimpleProductUpdate_productUpdate_product_metadata | null)[]; privateMetadata: (SimpleProductUpdate_productUpdate_product_privateMetadata | null)[]; name: string; + slug: string; descriptionJson: any; seoTitle: string | null; seoDescription: string | null; diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index c6154419c..2907367b3 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -182,6 +182,7 @@ export interface ProductUpdatePageFormData extends MetadataFormData { isAvailableForPurchase: boolean; isPublished: boolean; name: string; + slug: string; publicationDate: string; seoDescription: string; seoTitle: string; @@ -225,6 +226,7 @@ export function getProductUpdatePageFormData( : undefined, "" ), + slug: product?.slug || "", taxCode: product?.taxType.taxCode, trackInventory: !!product?.variants[0]?.trackInventory, visibleInListings: !!product?.visibleInListings, diff --git a/src/products/views/ProductCreate.tsx b/src/products/views/ProductCreate.tsx index 5d40ebffb..138e24e38 100644 --- a/src/products/views/ProductCreate.tsx +++ b/src/products/views/ProductCreate.tsx @@ -113,6 +113,7 @@ export const ProductCreateView: React.FC = () => { title: formData.seoTitle }, sku: formData.sku, + slug: formData.slug, stocks: formData.stocks.map(stock => ({ quantity: parseInt(stock.value, 0), warehouse: stock.id diff --git a/src/products/views/ProductUpdate/handlers.ts b/src/products/views/ProductUpdate/handlers.ts index feedc84a2..c68e32bf5 100644 --- a/src/products/views/ProductUpdate/handlers.ts +++ b/src/products/views/ProductUpdate/handlers.ts @@ -60,6 +60,7 @@ export function createUpdateHandler( description: data.seoDescription, title: data.seoTitle }, + slug: data.slug, taxCode: data.changeTaxCode ? data.taxCode : null, visibleInListings: data.visibleInListings } diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index f47ece1fe..4efe9fdd0 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -113433,73 +113433,6 @@ Ctrl + K"
-
-
- - URL - -
-
-
-
-
-
- -
- - -
-

- If empty, URL will be autogenerated from Page Name -

-
-
-
@@ -114324,73 +114257,6 @@ Ctrl + K"
-
-
- - URL - -
-
-
-
-
-
- -
- - -
-

- Invalid value -

-
-
-
@@ -115018,74 +114884,6 @@ Ctrl + K"
-
-
- - URL - -
-
-
-
-
-
- -
- - -
-

- If empty, URL will be autogenerated from Page Name -

-
-
-
diff --git a/src/utils/errors/page.ts b/src/utils/errors/page.ts index 500c9e60e..7784ad89d 100644 --- a/src/utils/errors/page.ts +++ b/src/utils/errors/page.ts @@ -1,10 +1,16 @@ import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import { commonMessages } from "@saleor/intl"; import { PageErrorCode } from "@saleor/types/globalTypes"; -import { IntlShape } from "react-intl"; +import { defineMessages, IntlShape } from "react-intl"; import commonErrorMessages from "./common"; +const messages = defineMessages({ + nameAlreadyTaken: { + defaultMessage: "This name is already taken. Please provide another." + } +}); + function getPageErrorMessage( err: Omit | undefined, intl: IntlShape @@ -17,6 +23,8 @@ function getPageErrorMessage( return intl.formatMessage(commonMessages.requiredField); case PageErrorCode.INVALID: return intl.formatMessage(commonErrorMessages.invalid); + case PageErrorCode.UNIQUE: + return intl.formatMessage(messages.nameAlreadyTaken); default: return intl.formatMessage(commonErrorMessages.unknownError); } diff --git a/src/utils/errors/product.ts b/src/utils/errors/product.ts index 61806ec48..2c1295765 100644 --- a/src/utils/errors/product.ts +++ b/src/utils/errors/product.ts @@ -27,6 +27,9 @@ const messages = defineMessages({ duplicatedInputItem: { defaultMessage: "Variant with these attributes already exists" }, + nameAlreadyTaken: { + defaultMessage: "This name is already taken. Please provide another." + }, skuUnique: { defaultMessage: "SKUs must be unique", description: "bulk variant create error" @@ -64,6 +67,8 @@ function getProductErrorMessage( return intl.formatMessage(messages.variantNoDigitalContent); case ProductErrorCode.INVALID: return intl.formatMessage(commonErrorMessages.invalid); + case ProductErrorCode.UNIQUE: + return intl.formatMessage(messages.nameAlreadyTaken); default: return intl.formatMessage(commonErrorMessages.unknownError); }