diff --git a/plugins/woo-ai/changelog/update-product-slug-ai b/plugins/woo-ai/changelog/update-product-slug-ai new file mode 100644 index 00000000000..5f6a89c4cec --- /dev/null +++ b/plugins/woo-ai/changelog/update-product-slug-ai @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Update the product's permalink (slug) when an AI suggested title is selected. diff --git a/plugins/woo-ai/src/hooks/index.ts b/plugins/woo-ai/src/hooks/index.ts index c9f68c701cb..8588128cf21 100644 --- a/plugins/woo-ai/src/hooks/index.ts +++ b/plugins/woo-ai/src/hooks/index.ts @@ -1,3 +1,5 @@ export * from './useTinyEditor'; export * from './useCompletion'; export * from './useFeedbackSnackbar'; +export * from './useProductSlug'; +export * from './useProductDataSuggestions'; diff --git a/plugins/woo-ai/src/hooks/useProductSlug.ts b/plugins/woo-ai/src/hooks/useProductSlug.ts new file mode 100644 index 00000000000..6e11a86c3cd --- /dev/null +++ b/plugins/woo-ai/src/hooks/useProductSlug.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { useRef } from '@wordpress/element'; + +type UseProductSlugHook = { + updateProductSlug: ( title: string, postId: number ) => Promise< void >; +}; + +declare global { + interface Window { + ajaxurl?: string; + } +} + +export const useProductSlug = (): UseProductSlugHook => { + const slugInputRef = useRef< HTMLInputElement >( + document.querySelector( '#post_name' ) + ); + + const updateSlugInDOM = ( responseData: string ) => { + const editSlugBox = document.getElementById( 'edit-slug-box' ); + if ( editSlugBox ) { + editSlugBox.innerHTML = responseData; + + const newSlug = document.getElementById( + 'editable-post-name-full' + )?.innerText; + if ( newSlug && slugInputRef.current ) { + slugInputRef.current.value = newSlug; + slugInputRef.current.setAttribute( 'value', newSlug ); + } + } + }; + + const updateProductSlug = async ( + title: string, + postId: number + ): Promise< void > => { + const ajaxUrl: string = window?.ajaxurl || '/wp-admin/admin-ajax.php'; + const samplePermalinkNonce = document + .getElementById( 'samplepermalinknonce' ) + ?.getAttribute( 'value' ); + + if ( ! samplePermalinkNonce ) { + throw new Error( 'Nonce could not be found in the DOM' ); + } + + const formData = new FormData(); + formData.append( 'action', 'sample-permalink' ); + formData.append( 'post_id', postId.toString() ); + formData.append( 'new_title', title ); + formData.append( 'new_slug', title ); + formData.append( 'samplepermalinknonce', samplePermalinkNonce ); + + try { + const response = await fetch( ajaxUrl, { + method: 'POST', + body: formData, + } ); + + const responseData = await response.text(); + + if ( responseData !== '-1' ) { + updateSlugInDOM( responseData ); + } else { + throw new Error( 'Invalid response!' ); + } + } catch ( e ) { + throw new Error( "Couldn't update the slug!" ); + } + }; + + return { + updateProductSlug, + }; +}; diff --git a/plugins/woo-ai/src/product-name/name-utils.ts b/plugins/woo-ai/src/product-name/name-utils.ts index e284d29747e..066051a9962 100644 --- a/plugins/woo-ai/src/product-name/name-utils.ts +++ b/plugins/woo-ai/src/product-name/name-utils.ts @@ -5,7 +5,7 @@ import { recordTracksFactory, getPostId } from '../utils'; type TracksData = Record< string, - string | number | Array< Record< string, string | number > > + string | number | null | Array< Record< string, string | number | null > > >; export const recordNameTracks = recordTracksFactory< TracksData >( diff --git a/plugins/woo-ai/src/product-name/product-name-suggestions.tsx b/plugins/woo-ai/src/product-name/product-name-suggestions.tsx index c854b48c313..716d30b0924 100644 --- a/plugins/woo-ai/src/product-name/product-name-suggestions.tsx +++ b/plugins/woo-ai/src/product-name/product-name-suggestions.tsx @@ -11,7 +11,7 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import MagicIcon from '../../assets/images/icons/magic.svg'; import AlertIcon from '../../assets/images/icons/alert.svg'; import { productData } from '../utils'; -import { useProductDataSuggestions } from '../hooks/useProductDataSuggestions'; +import { useProductDataSuggestions, useProductSlug } from '../hooks'; import { ProductDataSuggestion, ProductDataSuggestionRequest, @@ -56,6 +56,7 @@ export const ProductNameSuggestions = () => { [] ); const { fetchSuggestions } = useProductDataSuggestions(); + const { updateProductSlug } = useProductSlug(); const nameInputRef = useRef< HTMLInputElement >( document.querySelector( '#title' ) ); @@ -148,6 +149,24 @@ export const ProductNameSuggestions = () => { updateProductName( suggestion.content ); setSuggestions( [] ); + + // Update product slug if product is a draft. + const currentProductData = productData(); + if ( + currentProductData.product_id !== null && + currentProductData.publishing_status === 'draft' + ) { + try { + updateProductSlug( + suggestion.content, + currentProductData.product_id + ); + } catch ( e ) { + // Log silently if slug update fails. + /* eslint-disable-next-line no-console */ + console.error( e ); + } + } }; const fetchProductSuggestions = async ( diff --git a/plugins/woo-ai/src/utils/get-post-id.ts b/plugins/woo-ai/src/utils/get-post-id.ts index 66391a42fec..819fd8e3ac0 100644 --- a/plugins/woo-ai/src/utils/get-post-id.ts +++ b/plugins/woo-ai/src/utils/get-post-id.ts @@ -1,4 +1,6 @@ -export const getPostId = () => - Number( - ( document.querySelector( '#post_ID' ) as HTMLInputElement )?.value - ); +export const getPostId = (): number | null => { + const postIdEl: HTMLInputElement | null = + document.querySelector( '#post_ID' ); + + return postIdEl ? Number( postIdEl.value ) : null; +}; diff --git a/plugins/woo-ai/src/utils/productData.ts b/plugins/woo-ai/src/utils/productData.ts index 8b54ae3ebcb..672165af7ea 100644 --- a/plugins/woo-ai/src/utils/productData.ts +++ b/plugins/woo-ai/src/utils/productData.ts @@ -2,7 +2,7 @@ * Internal dependencies */ import { Attribute, ProductData } from './types'; -import { getTinyContent } from '../utils/tiny-tools'; +import { getTinyContent, getPostId } from '.'; const isElementVisible = ( element: HTMLElement ) => ! ( window.getComputedStyle( element ).display === 'none' ); @@ -104,6 +104,7 @@ const getProductType = () => { export const productData = (): ProductData => { return { + product_id: getPostId(), name: getProductName(), categories: getCategories(), tags: getTags(), @@ -112,13 +113,12 @@ export const productData = (): ProductData => { product_type: getProductType(), is_downloadable: ( document.querySelector( '#_downloadable' ) as HTMLInputElement - )?.checked - ? 'Yes' - : 'No', + )?.checked, is_virtual: ( document.querySelector( '#_virtual' ) as HTMLInputElement - )?.checked - ? 'Yes' - : 'No', + )?.checked, + publishing_status: ( + document.querySelector( '#post_status' ) as HTMLInputElement + )?.value, }; }; diff --git a/plugins/woo-ai/src/utils/recordTracksFactory.ts b/plugins/woo-ai/src/utils/recordTracksFactory.ts index 71bffdde7ba..4fb68f2e8c8 100644 --- a/plugins/woo-ai/src/utils/recordTracksFactory.ts +++ b/plugins/woo-ai/src/utils/recordTracksFactory.ts @@ -3,9 +3,11 @@ */ import { recordEvent } from '@woocommerce/tracks'; -export const recordTracksFactory = < T = Record< string, string | number > >( +export const recordTracksFactory = < + T = Record< string, string | number | null > +>( feature: string, - propertiesCallback: () => Record< string, string | number > + propertiesCallback: () => Record< string, string | number | null > ) => { return ( name: string, properties?: T ) => recordEvent( `woo_ai_product_${ feature }_${ name }`, { diff --git a/plugins/woo-ai/src/utils/types.ts b/plugins/woo-ai/src/utils/types.ts index 14c6c18f709..43a61edd22f 100644 --- a/plugins/woo-ai/src/utils/types.ts +++ b/plugins/woo-ai/src/utils/types.ts @@ -4,14 +4,16 @@ export type Attribute = { }; export type ProductData = { + product_id: number | null; name: string; description: string; categories: string[]; tags: string[]; attributes: Attribute[]; product_type: string; - is_downloadable: string; - is_virtual: string; + is_downloadable: boolean; + is_virtual: boolean; + publishing_status: string; }; export type ProductDataSuggestion = {