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 './useTinyEditor';
|
||||||
export * from './useCompletion';
|
export * from './useCompletion';
|
||||||
export * from './useFeedbackSnackbar';
|
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<
|
type TracksData = Record<
|
||||||
string,
|
string,
|
||||||
string | number | Array< Record< string, string | number > >
|
string | number | null | Array< Record< string, string | number | null > >
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const recordNameTracks = recordTracksFactory< TracksData >(
|
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 MagicIcon from '../../assets/images/icons/magic.svg';
|
||||||
import AlertIcon from '../../assets/images/icons/alert.svg';
|
import AlertIcon from '../../assets/images/icons/alert.svg';
|
||||||
import { productData } from '../utils';
|
import { productData } from '../utils';
|
||||||
import { useProductDataSuggestions } from '../hooks/useProductDataSuggestions';
|
import { useProductDataSuggestions, useProductSlug } from '../hooks';
|
||||||
import {
|
import {
|
||||||
ProductDataSuggestion,
|
ProductDataSuggestion,
|
||||||
ProductDataSuggestionRequest,
|
ProductDataSuggestionRequest,
|
||||||
|
@ -56,6 +56,7 @@ export const ProductNameSuggestions = () => {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const { fetchSuggestions } = useProductDataSuggestions();
|
const { fetchSuggestions } = useProductDataSuggestions();
|
||||||
|
const { updateProductSlug } = useProductSlug();
|
||||||
const nameInputRef = useRef< HTMLInputElement >(
|
const nameInputRef = useRef< HTMLInputElement >(
|
||||||
document.querySelector( '#title' )
|
document.querySelector( '#title' )
|
||||||
);
|
);
|
||||||
|
@ -148,6 +149,24 @@ export const ProductNameSuggestions = () => {
|
||||||
|
|
||||||
updateProductName( suggestion.content );
|
updateProductName( suggestion.content );
|
||||||
setSuggestions( [] );
|
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 (
|
const fetchProductSuggestions = async (
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export const getPostId = () =>
|
export const getPostId = (): number | null => {
|
||||||
Number(
|
const postIdEl: HTMLInputElement | null =
|
||||||
( document.querySelector( '#post_ID' ) as HTMLInputElement )?.value
|
document.querySelector( '#post_ID' );
|
||||||
);
|
|
||||||
|
return postIdEl ? Number( postIdEl.value ) : null;
|
||||||
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Attribute, ProductData } from './types';
|
import { Attribute, ProductData } from './types';
|
||||||
import { getTinyContent } from '../utils/tiny-tools';
|
import { getTinyContent, getPostId } from '.';
|
||||||
|
|
||||||
const isElementVisible = ( element: HTMLElement ) =>
|
const isElementVisible = ( element: HTMLElement ) =>
|
||||||
! ( window.getComputedStyle( element ).display === 'none' );
|
! ( window.getComputedStyle( element ).display === 'none' );
|
||||||
|
@ -104,6 +104,7 @@ const getProductType = () => {
|
||||||
|
|
||||||
export const productData = (): ProductData => {
|
export const productData = (): ProductData => {
|
||||||
return {
|
return {
|
||||||
|
product_id: getPostId(),
|
||||||
name: getProductName(),
|
name: getProductName(),
|
||||||
categories: getCategories(),
|
categories: getCategories(),
|
||||||
tags: getTags(),
|
tags: getTags(),
|
||||||
|
@ -112,13 +113,12 @@ export const productData = (): ProductData => {
|
||||||
product_type: getProductType(),
|
product_type: getProductType(),
|
||||||
is_downloadable: (
|
is_downloadable: (
|
||||||
document.querySelector( '#_downloadable' ) as HTMLInputElement
|
document.querySelector( '#_downloadable' ) as HTMLInputElement
|
||||||
)?.checked
|
)?.checked,
|
||||||
? 'Yes'
|
|
||||||
: 'No',
|
|
||||||
is_virtual: (
|
is_virtual: (
|
||||||
document.querySelector( '#_virtual' ) as HTMLInputElement
|
document.querySelector( '#_virtual' ) as HTMLInputElement
|
||||||
)?.checked
|
)?.checked,
|
||||||
? 'Yes'
|
publishing_status: (
|
||||||
: 'No',
|
document.querySelector( '#post_status' ) as HTMLInputElement
|
||||||
|
)?.value,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
*/
|
*/
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
export const recordTracksFactory = < T = Record< string, string | number > >(
|
export const recordTracksFactory = <
|
||||||
|
T = Record< string, string | number | null >
|
||||||
|
>(
|
||||||
feature: string,
|
feature: string,
|
||||||
propertiesCallback: () => Record< string, string | number >
|
propertiesCallback: () => Record< string, string | number | null >
|
||||||
) => {
|
) => {
|
||||||
return ( name: string, properties?: T ) =>
|
return ( name: string, properties?: T ) =>
|
||||||
recordEvent( `woo_ai_product_${ feature }_${ name }`, {
|
recordEvent( `woo_ai_product_${ feature }_${ name }`, {
|
||||||
|
|
|
@ -4,14 +4,16 @@ export type Attribute = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProductData = {
|
export type ProductData = {
|
||||||
|
product_id: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
attributes: Attribute[];
|
attributes: Attribute[];
|
||||||
product_type: string;
|
product_type: string;
|
||||||
is_downloadable: string;
|
is_downloadable: boolean;
|
||||||
is_virtual: string;
|
is_virtual: boolean;
|
||||||
|
publishing_status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProductDataSuggestion = {
|
export type ProductDataSuggestion = {
|
||||||
|
|
Loading…
Reference in New Issue