Skip to content

Commit 7dc7058

Browse files
committed
Added the ability to click and edit AI terms
1 parent fe64d4c commit 7dc7058

File tree

2 files changed

+212
-8
lines changed

2 files changed

+212
-8
lines changed

src/js/features/classification/taxonomy-controls.js

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FormTokenField } from '@wordpress/components';
88
import { useSelect } from '@wordpress/data';
99
import { store as coreStore } from '@wordpress/core-data';
1010
import { getEntitiesInfo, useTaxonomies } from './utils';
11-
import { useState, Fragment } from '@wordpress/element';
11+
import { useState, Fragment, useRef, useEffect, useCallback } from '@wordpress/element';
1212
import { __, sprintf } from '@wordpress/i18n';
1313

1414
const termsPerPage = -1;
@@ -47,6 +47,10 @@ const TaxonomyControls = ( { onChange, query } ) => {
4747
const taxTermsAI = query.taxTermsAI || [];
4848
const [ newTermsInfo, setNewTermsInfo ] = useState( {} );
4949

50+
// State for editing AI tokens inline
51+
const [ editingToken, setEditingToken ] = useState( null );
52+
const tokenFieldRefs = useRef( {} );
53+
5054
const appendAIPrefix = ( terms, slug ) => {
5155
if (
5256
undefined !== terms &&
@@ -99,6 +103,168 @@ const TaxonomyControls = ( { onChange, query } ) => {
99103
taxonomiesInfo = newTermsInfo;
100104
}
101105

106+
/**
107+
* Handle clicking on an [AI] token to edit it inline.
108+
* This allows users to tweak AI-suggested terms before saving.
109+
*
110+
* @param {string} taxonomySlug The taxonomy slug.
111+
* @param {number} termId The term ID being edited.
112+
* @param {string} termName The term name (with [AI] prefix).
113+
*/
114+
const handleAITokenClick = useCallback(
115+
( taxonomySlug, termId, termName ) => {
116+
// Extract the term name without the [AI] prefix
117+
const cleanName = termName.replace( /^\[AI\]\s*/, '' );
118+
119+
// Store the editing state
120+
setEditingToken( {
121+
taxonomySlug,
122+
termId,
123+
originalName: termName,
124+
cleanName,
125+
} );
126+
127+
// Get current terms and remove the clicked AI term
128+
const currentTerms = query.taxQuery[ taxonomySlug ] || [];
129+
const termValues = Object.values( currentTerms );
130+
const newTermValues = termValues.filter( ( id ) => id !== termId );
131+
132+
// Update the taxQuery to remove the AI term
133+
const newTaxQuery = {
134+
...query.taxQuery,
135+
[ taxonomySlug ]: newTermValues.reduce(
136+
( acc, id, idx ) => ( { ...acc, [ idx ]: id } ),
137+
{}
138+
),
139+
};
140+
141+
onChange( { taxQuery: newTaxQuery } );
142+
143+
// Focus the input and pre-fill with the clean term name
144+
setTimeout( () => {
145+
const fieldRef = tokenFieldRefs.current[ taxonomySlug ];
146+
if ( fieldRef ) {
147+
const input = fieldRef.querySelector(
148+
'.components-form-token-field__input'
149+
);
150+
if ( input ) {
151+
input.value = cleanName;
152+
input.focus();
153+
// Trigger input event to update React state
154+
const event = new Event( 'input', { bubbles: true } );
155+
input.dispatchEvent( event );
156+
}
157+
}
158+
}, 50 );
159+
},
160+
[ query.taxQuery, onChange ]
161+
);
162+
163+
/**
164+
* Mark [AI] tokens as editable (for styling) after render.
165+
*/
166+
useEffect( () => {
167+
const markEditableTokens = () => {
168+
Object.keys( tokenFieldRefs.current ).forEach( ( taxonomySlug ) => {
169+
const fieldRef = tokenFieldRefs.current[ taxonomySlug ];
170+
if ( ! fieldRef ) {
171+
return;
172+
}
173+
174+
// Find all token buttons that contain [AI] text
175+
const tokens = fieldRef.querySelectorAll(
176+
'.components-form-token-field__token'
177+
);
178+
179+
tokens.forEach( ( token ) => {
180+
const tokenText =
181+
token.querySelector(
182+
'.components-form-token-field__token-text'
183+
)?.textContent || '';
184+
185+
if ( tokenText.includes( '[AI]' ) ) {
186+
// Add clickable class for styling
187+
token.classList.add( 'classifai-ai-token--editable' );
188+
189+
// Add tooltip to token text
190+
const tokenTextEl = token.querySelector(
191+
'.components-form-token-field__token-text'
192+
);
193+
if ( tokenTextEl ) {
194+
tokenTextEl.title = __(
195+
'Click to edit this term',
196+
'classifai'
197+
);
198+
}
199+
}
200+
} );
201+
} );
202+
};
203+
204+
// Small delay to ensure DOM is updated
205+
const timeoutId = setTimeout( markEditableTokens, 100 );
206+
return () => clearTimeout( timeoutId );
207+
}, [ taxonomiesInfo, query.taxQuery ] );
208+
209+
/**
210+
* Handle click events on the wrapper using event delegation.
211+
* This avoids stale closure issues with direct event listeners.
212+
*
213+
* @param {string} taxonomySlug The taxonomy slug for this field.
214+
* @return {Function} Click handler function.
215+
*/
216+
const handleWrapperClick = useCallback(
217+
( taxonomySlug ) => ( e ) => {
218+
// Check if the click was on a token text element
219+
const tokenTextEl = e.target.closest(
220+
'.components-form-token-field__token-text'
221+
);
222+
if ( ! tokenTextEl ) {
223+
return;
224+
}
225+
226+
// Get the token element and check if it's an AI token
227+
const tokenEl = tokenTextEl.closest(
228+
'.components-form-token-field__token'
229+
);
230+
if (
231+
! tokenEl ||
232+
! tokenEl.classList.contains( 'classifai-ai-token--editable' )
233+
) {
234+
return;
235+
}
236+
237+
// Get the token text
238+
const tokenText = tokenTextEl.textContent || '';
239+
if ( ! tokenText.includes( '[AI]' ) ) {
240+
return;
241+
}
242+
243+
e.preventDefault();
244+
e.stopPropagation();
245+
246+
// Find the term ID from the token
247+
const taxonomyInfo = taxonomiesInfo?.find(
248+
( { slug } ) => slug === taxonomySlug
249+
);
250+
if ( taxonomyInfo ) {
251+
// Find the term by matching the name
252+
const termEntry = Object.entries(
253+
taxonomyInfo.terms.mapById
254+
).find( ( [ , term ] ) => term.name === tokenText );
255+
256+
if ( termEntry ) {
257+
handleAITokenClick(
258+
taxonomySlug,
259+
parseInt( termEntry[ 0 ], 10 ),
260+
tokenText
261+
);
262+
}
263+
}
264+
},
265+
[ taxonomiesInfo, handleAITokenClick ]
266+
);
267+
102268
const onTermsChange = ( taxonomySlug ) => async ( newTermValues ) => {
103269
const taxonomyInfo = taxonomiesInfo.find(
104270
( { slug } ) => slug === taxonomySlug
@@ -272,13 +438,22 @@ const TaxonomyControls = ( { onChange, query } ) => {
272438

273439
return (
274440
<Fragment key={ slug }>
275-
<FormTokenField
276-
key={ slug }
277-
label={ name }
278-
value={ getExistingTaxQueryValue( slug ) }
279-
suggestions={ terms.names }
280-
onChange={ onTermsChange( slug ) }
281-
/>
441+
{ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ }
442+
<div
443+
ref={ ( el ) => {
444+
tokenFieldRefs.current[ slug ] = el;
445+
} }
446+
className="classifai-taxonomy-field-wrapper"
447+
onClick={ handleWrapperClick( slug ) }
448+
>
449+
<FormTokenField
450+
key={ slug }
451+
label={ name }
452+
value={ getExistingTaxQueryValue( slug ) }
453+
suggestions={ terms.names }
454+
onChange={ onTermsChange( slug ) }
455+
/>
456+
</div>
282457
{ ! hasAI && (
283458
<>
284459
<p

src/scss/admin.scss

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,35 @@ input.classifai-button {
883883
}
884884
}
885885

886+
// Editable AI Token Styles
887+
.classifai-taxonomy-field-wrapper {
888+
.classifai-ai-token--editable {
889+
.components-form-token-field__token-text {
890+
cursor: pointer;
891+
position: relative;
892+
border-bottom: 1px dashed var(--classifai-admin-theme-color, #007cba);
893+
transition: all 0.15s ease;
894+
895+
&:hover {
896+
background-color: rgba(var(--classifai-admin-theme-color--rgb, 0, 124, 186), 0.1);
897+
border-bottom-style: solid;
898+
}
899+
900+
&::before {
901+
content: "\270E";
902+
font-size: 10px;
903+
margin-right: 3px;
904+
opacity: 0;
905+
transition: opacity 0.15s ease;
906+
}
907+
908+
&:hover::before {
909+
opacity: 0.7;
910+
}
911+
}
912+
}
913+
}
914+
886915
.classifai-modal__notes {
887916
max-width: 350px;
888917
float: left;

0 commit comments

Comments
 (0)