/* eslint-disable no-nested-ternary */ import apiFetch from '@wordpress/api-fetch'; import { Button, Notice, SelectControl, Spinner, TextControl } from '@wordpress/components'; import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getNoTopicClusterAssignedYetLabel } from '../../shared/i18nPhrases'; import { useSelect } from '@wordpress/data'; import { getEditorConfig } from '../config'; type TopicClusterItem = { id: number; title: string; level: 'L1' | 'L2'; parent_post_id?: number | null; cluster_root_id?: number | null; }; type TopicClusterResponse = { items?: TopicClusterItem[]; current?: { post_id: number; level: 'L1' | 'L2' | 'L3'; parent_post_id?: number | null; cluster_root_id?: number | null; } | null; }; type TopicClusterSummary = { current?: { post_id: number; level: 'L1' | 'L2' | 'L3'; } | null; l1?: { id: number; title: string; edit?: string | null; l2?: number; l3?: number; } | null; l2?: { id: number; title: string; edit?: string | null; l3?: number; } | null; group?: { id?: number; name?: string; mind_map_url?: string; } | null; }; const DEFAULT_LEVEL = '' as const; const SettingsTabIcon = () => ( ); const MindMapTabIcon = () => ( ); const TopicClusterPanel = () => { const config = getEditorConfig().topicCluster; const postId = useSelect( ( select ) => { const editor = select( 'core/editor' ) as { getCurrentPostId?: () => number | undefined }; return editor.getCurrentPostId ? editor.getCurrentPostId() : undefined; }, [], ); const [ isLoading, setIsLoading ] = useState( false ); const [ isSaving, setIsSaving ] = useState( false ); const [ error, setError ] = useState< string | null >( null ); const [ items, setItems ] = useState< TopicClusterItem[] >( [] ); const [ level, setLevel ] = useState< '' | 'L1' | 'L2' | 'L3' >( DEFAULT_LEVEL ); const [ parentId, setParentId ] = useState< number | null >( null ); const [ parentSearch, setParentSearch ] = useState( '' ); const [ currentLevel, setCurrentLevel ] = useState< '' | 'L1' | 'L2' | 'L3' >( DEFAULT_LEVEL ); const [ activeTab, setActiveTab ] = useState< 'settings' | 'mindmap' >( 'settings' ); const [ summary, setSummary ] = useState< TopicClusterSummary | null >( null ); const l1Items = useMemo( () => items.filter( ( item ) => item.level === 'L1' ), [ items ], ); const l2Items = useMemo( () => items.filter( ( item ) => item.level === 'L2' ), [ items ], ); const loadData = useCallback( async () => { if ( ! config?.list || ! postId ) { return; } setIsLoading( true ); setError( null ); try { const query = new URLSearchParams( { post: String( postId ) } ); const data = await apiFetch< TopicClusterResponse >( { url: `${ config.list }?${ query.toString() }`, method: 'GET', headers: { 'X-WP-Nonce': config.nonce ?? '' }, } ); const nextItems = Array.isArray( data.items ) ? data.items : []; setItems( nextItems ); if ( data.current && data.current.level ) { setLevel( data.current.level ); setCurrentLevel( data.current.level ); setParentId( data.current.parent_post_id ? Number( data.current.parent_post_id ) : null, ); } else { setLevel( DEFAULT_LEVEL ); setCurrentLevel( DEFAULT_LEVEL ); setParentId( null ); } } catch ( err ) { const message = err && typeof err === 'object' && 'message' in err ? String( ( err as Error ).message ) : __( 'Unable to load Topic Cluster settings.', 'airygen-seo' ); setError( message ); } finally { setIsLoading( false ); } }, [ config?.list, config?.nonce, postId ] ); useEffect( () => { if ( postId && config?.list ) { void loadData(); } }, [ postId, config?.list, loadData ] ); const saveSettings = useCallback( async () => { if ( ! config?.save || ! postId ) { return; } setIsSaving( true ); setError( null ); try { await apiFetch( { url: config.save, method: 'POST', headers: { 'X-WP-Nonce': config.nonce ?? '' }, data: { post: postId, level, parent_post_id: parentId ?? 0, }, } ); } catch ( err ) { const message = err && typeof err === 'object' && 'message' in err ? String( ( err as Error ).message ) : __( 'Unable to save Topic Cluster settings.', 'airygen-seo' ); setError( message ); } finally { setIsSaving( false ); } }, [ config?.save, config?.nonce, level, parentId, postId ] ); const loadSummary = useCallback( async () => { if ( ! config?.summary || ! postId ) { return; } try { const query = new URLSearchParams( { post: String( postId ) } ); const data = await apiFetch< TopicClusterSummary >( { url: `${ config.summary }?${ query.toString() }`, method: 'GET', headers: { 'X-WP-Nonce': config.nonce ?? '' }, } ); setSummary( data ); } catch ( err ) { const message = err && typeof err === 'object' && 'message' in err ? String( ( err as Error ).message ) : __( 'Unable to load Topic Cluster summary.', 'airygen-seo' ); setError( message ); } }, [ config?.summary, config?.nonce, postId ] ); useEffect( () => { if ( activeTab === 'mindmap' ) { void loadSummary(); } }, [ activeTab, loadSummary ] ); const parentOptions = useMemo( () => { const base = level === 'L2' ? l1Items : level === 'L3' ? l2Items : []; return [ { value: '', label: __( 'Select parent…', 'airygen-seo' ) }, ...base.map( ( item ) => ( { value: String( item.id ), label: item.title, } ) ), ]; }, [ level, l1Items, l2Items ] ); const filteredParentOptions = useMemo( () => { if ( level !== 'L2' && level !== 'L3' ) { return parentOptions; } const query = parentSearch.trim().toLowerCase(); if ( '' === query ) { return parentOptions; } const defaultOption = parentOptions[ 0 ] ?? { value: '', label: __( 'Select parent…', 'airygen-seo' ), }; const matched = parentOptions .slice( 1 ) .filter( ( option ) => option.label.toLowerCase().includes( query ) ); const selectedValue = parentId ? String( parentId ) : ''; if ( selectedValue && ! matched.some( ( option ) => option.value === selectedValue ) ) { const selectedOption = parentOptions.find( ( option ) => option.value === selectedValue ); if ( selectedOption ) { matched.unshift( selectedOption ); } } return [ defaultOption, ...matched ]; }, [ level, parentOptions, parentSearch, parentId ] ); const hasChildren = useMemo( () => { if ( ! postId ) { return false; } return items.some( ( item ) => item.parent_post_id === postId ); }, [ items, postId ] ); const isLevelChangeBlocked = Boolean( currentLevel ) && currentLevel !== level && hasChildren; return (
{ error ? ( { error } ) : null } { activeTab === 'mindmap' ? ( <>
{ ! summary?.current?.level ? (
{ getNoTopicClusterAssignedYetLabel() }
) : (
{ __( 'Level', 'airygen-seo' ) } { __( 'Details', 'airygen-seo' ) }
{ __( 'Group', 'airygen-seo' ) } { summary?.group?.name || '—' }
L1 { summary?.current?.level === 'L1' ? ( { __( 'This post is a pillar.', 'airygen-seo' ) } ) : summary?.l1?.title ? ( { summary?.l1?.title } ) : ( ) }
L2 { summary?.current?.level === 'L1' ? ( { __( 'Total:', 'airygen-seo' ) } { summary?.l1?.l2 ?? 0 } ) : summary?.current?.level === 'L2' ? ( { __( 'This post is a cluster.', 'airygen-seo' ) } ) : summary?.current?.level === 'L3' && summary?.l2?.title ? ( { summary?.l2?.title } ) : ( ) }
L3 { summary?.current?.level === 'L3' ? ( { __( 'This post is a support article.', 'airygen-seo' ) } ) : summary?.current?.level === 'L2' ? ( { __( 'Total:', 'airygen-seo' ) } { summary?.l2?.l3 ?? 0 } ) : summary?.current?.level === 'L1' ? ( { __( 'Total:', 'airygen-seo' ) } { summary?.l1?.l3 ?? 0 } ) : ( ) }
) }
) : null } { activeTab === 'settings' ? ( <> { const next = value as '' | 'L1' | 'L2' | 'L3'; setLevel( next ); setParentSearch( '' ); if ( next === 'L1' ) { setParentId( null ); } } } /> { level === 'L1' ? (

{ __( 'This post becomes the root of a new topic cluster.', 'airygen-seo' ) }

) : null } { ( level === 'L2' || level === 'L3' ) ? ( <> setParentSearch( value ) } placeholder={ __( 'Type to filter parent posts…', 'airygen-seo' ) } /> { const nextId = value ? Number( value ) : null; setParentId( nextId ); } } /> { `${ Math.max( 0, filteredParentOptions.length - 1 ) } ${ __( 'matching parent posts.', 'airygen-seo' ) }` } ) : null }
{ isLevelChangeBlocked ? ( { __( 'This post already has child items. Remove them before changing the level.', 'airygen-seo', ) } ) : null } { isLoading ? : null }
) : null }
); }; export default TopicClusterPanel;