Update the product's permalink (slug) when an AI suggestion is selected (#38902)
* Add product id to product data * Create a React Hook for updating product slug * Update product slug when title is updated. * Add changelog * Import hooks from index * Use getPostId util to get product ID * Only update draft product's slug
This commit is contained in:
parent
783cfd4f29
commit
500cdb8b23
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Update the product's permalink (slug) when an AI suggested title is selected.
|
|
@ -1,3 +1,5 @@
|
|||
export * from './useTinyEditor';
|
||||
export * from './useCompletion';
|
||||
export * from './useFeedbackSnackbar';
|
||||
export * from './useProductSlug';
|
||||
export * from './useProductDataSuggestions';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 >(
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 }`, {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue