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:
Nima Karimi 2023-06-27 21:46:18 +03:00 committed by GitHub
parent 783cfd4f29
commit 500cdb8b23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 125 additions and 17 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Update the product's permalink (slug) when an AI suggested title is selected.

View File

@ -1,3 +1,5 @@
export * from './useTinyEditor';
export * from './useCompletion';
export * from './useFeedbackSnackbar';
export * from './useProductSlug';
export * from './useProductDataSuggestions';

View File

@ -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,
};
};

View File

@ -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 >(

View File

@ -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 (

View File

@ -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;
};

View File

@ -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,
};
};

View File

@ -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 }`, {

View File

@ -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 = {