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 ) }
/>
);
} ) }
) }
);
}