import { useState, useEffect } from '@wordpress/element'; import { Button, SelectControl, TextControl, CheckboxControl, Spinner, } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; const JOIN_TYPES = [ { label: 'Inner (only matching rows)', value: 'inner' }, { label: 'Left (all primary, matched target)', value: 'left' }, { label: 'Right (all target, matched primary)', value: 'right' }, ]; const JOINABLE_BLOCK_TYPES = [ { label: 'Posts', value: 'posts' }, { label: 'Users', value: 'users' }, { label: 'Comments', value: 'comments' }, ]; export default function JoinSelector( { blockConfig, blockType, onChange } ) { const joins = blockConfig.joins || []; const postTypes = window.wlconduitData?.postTypes || []; function updateJoin( index, partial ) { const updated = [ ...joins ]; updated[ index ] = { ...updated[ index ], ...partial }; onChange( updated ); } function addJoin() { onChange( [ ...joins, { label: '', target_type: 'posts', target_post_type: 'post', local_field: '', local_field_source: 'native', local_field_from: 'self', target_field: '', target_field_source: 'native', join_type: 'inner', target_fields: [], target_meta_keys: {}, }, ] ); } function removeJoin( index ) { const updated = joins.filter( ( _, i ) => i !== index ); onChange( updated ); } return (

Join data from other tables into each result row. Chain multiple joins for complex relationships.

{ joins.map( ( join, index ) => ( updateJoin( index, partial ) } onRemove={ () => removeJoin( index ) } /> ) ) }
); } function JoinEntry( { index, join, joins, blockType, blockConfig, postTypes, onChange, onRemove } ) { const [ myFieldOptions, setMyFieldOptions ] = useState( [] ); const [ targetColumns, setTargetColumns ] = useState( {} ); const [ targetMeta, setTargetMeta ] = useState( [] ); const [ targetFields, setTargetFields ] = useState( {} ); const [ loadingTarget, setLoadingTarget ] = useState( false ); // Build "My field" options from primary block + previous joins. useEffect( () => { const options = []; apiFetch( { path: `wlconduit/v1/join-columns?block_type=${ blockType }` } ) .then( ( columns ) => { const group = { label: 'Primary', options: [] }; Object.entries( columns ).forEach( ( [ col, label ] ) => { group.options.push( { label: `${ label } (${ col })`, value: `self:${ col }` } ); } ); options.push( group ); // Previous joins' columns. const promises = []; for ( let i = 0; i < index; i++ ) { const prevJoin = joins[ i ]; if ( prevJoin && prevJoin.target_type ) { promises.push( apiFetch( { path: `wlconduit/v1/join-columns?block_type=${ prevJoin.target_type }` } ) .then( ( cols ) => ( { index: i, join: prevJoin, columns: cols } ) ) ); } } return Promise.all( promises ); } ) .then( ( prevResults ) => { if ( prevResults ) { prevResults.forEach( ( prev ) => { const groupLabel = prev.join.label || `Join ${ prev.index + 1 }`; const group = { label: groupLabel, options: [] }; Object.entries( prev.columns ).forEach( ( [ col, label ] ) => { group.options.push( { label: `${ label } (${ col })`, value: `${ prev.index }:${ col }`, } ); } ); options.push( group ); } ); } setMyFieldOptions( options ); } ) .catch( () => {} ); }, [ blockType, blockConfig.post_type, blockConfig.meta_keys, index, joins.length ] ); // Load target data when target type or post type changes. useEffect( () => { if ( ! join.target_type ) { return; } setLoadingTarget( true ); // Load native columns. const columnsPromise = apiFetch( { path: `wlconduit/v1/join-columns?block_type=${ join.target_type }` } ) .then( ( data ) => setTargetColumns( data || {} ) ) .catch( () => setTargetColumns( {} ) ); // Load available fields from block definition. const blockDef = window.wlconduitData?.blocks?.[ join.target_type ]; setTargetFields( blockDef?.fields || {} ); // Load meta keys. Comments meta isn't exposed in the join builder // (commentmeta is rarely populated and not part of the supported feature set). let metaPath = ''; if ( join.target_type === 'users' ) { metaPath = 'wlconduit/v1/meta-keys?object_type=user'; } else if ( join.target_type === 'posts' && join.target_post_type ) { metaPath = `wlconduit/v1/meta-keys?object_type=post&post_type=${ join.target_post_type }`; } else if ( join.target_type === 'posts' ) { metaPath = 'wlconduit/v1/meta-keys?object_type=post'; } const metaPromise = metaPath ? apiFetch( { path: metaPath } ) .then( ( data ) => setTargetMeta( data || [] ) ) .catch( () => setTargetMeta( [] ) ) : Promise.resolve( setTargetMeta( [] ) ); Promise.all( [ columnsPromise, metaPromise ] ) .finally( () => setLoadingTarget( false ) ); }, [ join.target_type, join.target_post_type ] ); function handleMyFieldChange( value ) { if ( ! value ) { return; } const parts = value.split( ':' ); onChange( { local_field_from: parts[ 0 ] === 'self' ? 'self' : parseInt( parts[ 0 ], 10 ), local_field: parts[ 1 ] || '', } ); } function handleTargetFieldChange( value ) { if ( ! value ) { return; } onChange( { target_field: value } ); } function toggleTargetField( field ) { const current = join.target_fields || []; if ( current.includes( field ) ) { onChange( { target_fields: current.filter( ( f ) => f !== field ) } ); } else { onChange( { target_fields: [ ...current, field ] } ); } } function toggleTargetMeta( key ) { const current = join.target_meta_keys || {}; if ( current[ key ] ) { const updated = { ...current }; delete updated[ key ]; onChange( { target_meta_keys: updated } ); } else { onChange( { target_meta_keys: { ...current, [ key ]: { as_field: true } } } ); } } // Current values for selects. const myFieldValue = join.local_field ? `${ join.local_field_from === 'self' ? 'self' : join.local_field_from }:${ join.local_field }` : ''; const targetFieldValue = join.target_field || ''; // Flatten my field options for SelectControl. const flatMyOptions = [ { label: '-- Select --', value: '' } ]; myFieldOptions.forEach( ( group ) => { flatMyOptions.push( { label: `── ${ group.label } ──`, value: '', disabled: true } ); group.options.forEach( ( opt ) => flatMyOptions.push( opt ) ); } ); // Their field options — native columns only. const theirOptions = [ { label: '-- Select --', value: '' } ]; Object.entries( targetColumns ).forEach( ( [ col, label ] ) => { theirOptions.push( { label: `${ label } (${ col })`, value: col } ); } ); return (
Join { index + 1 }
onChange( { label: val } ) } /> onChange( { target_type: val, target_post_type: val === 'posts' ? 'post' : '', target_fields: [], target_meta_keys: {}, } ) } /> { join.target_type === 'posts' && ( ( { label: pt.internal ? `${ pt.label } (internal)` : pt.label, value: pt.name } ) ) } onChange={ ( val ) => onChange( { target_post_type: val, target_meta_keys: {}, } ) } /> ) } { loadingTarget ? ( ) : ( ) } onChange( { join_type: val } ) } />

Fields from joined table

{ Object.entries( targetFields ).map( ( [ key, label ] ) => ( toggleTargetField( key ) } /> ) ) }
{ targetMeta.length > 0 && (

Custom fields from joined table

{ targetMeta.map( ( item ) => { const key = item.key || item; const isInternal = item.internal || false; return ( toggleTargetMeta( key ) } /> ); } ) }
) }
); }