admin_search-options_src_components_indexableFields.js
import { __, _x } from '@wordpress/i18n';
import { Repeater } from './repeater';
import { useEffect, useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { useFirstRender } from '../utils/useFirstRender';
import AddIndexableField from './addIndexableField';
import ConfirmDialog from './confirmDialog';
import {
Button,
Spinner,
ToggleControl,
SelectControl,
} from '@wordpress/components';
export default function indexableField( props ) {
const [ indexableFields, setIndexableFields ] = useState(
findstr.settingsIndexableFields || {}
);
const [ fieldToDelete, setFieldToDelete ] = useState( false );
const [ isLoading, setIsLoading ] = useState( false );
const firstRender = useFirstRender();
/**
* Fields that can't be deleted
*
* @hook findstrUnDeletableFields
* @param {Array} unDeletableFields
*
* @return {Array} fields
*/
const unDeletableFields = findstr.hooks.applyFilters(
'findstrUnDeletableFields',
[ 'ID', 'language', 'post_content' ]
);
/**
* Filter fields that can't be filterable
* @hook findstrUnFilterableFields
*
* @param {Array} unFilterableFields
*
* @return {Array} fields
*/
const fieldsDisabledFilterableEdit = findstr.hooks.applyFilters(
'findstrUnFilterableFields',
[ 'post_content', 'language' ]
);
const [ isAddFieldFormOpen, openAddFieldForm ] = useState( false );
/**
* Filter field types.
*
* Field types are used to differentiate them in indexing process, for example for the date type,
* who need to be transformed in timestamp.
*
* @hook findstrFieldTypes
*
* @param {Array} fieldTypes
*
* @return {Array} fieldTypes
*/
const fieldTypes = findstr.hooks.applyFilters( 'findstrFieldTypes', [
{ value: 'text', label: _x( 'Text', 'field type', 'findstr' ) },
{ value: 'date', label: _x( 'Date', 'field type', 'findstr' ) },
] );
function deleteIndexableField( field ) {
setFieldToDelete( field );
}
//
function updateIndexableField() {
if ( firstRender ) {
return;
}
//clean indexable fields before sending to the server
const cleanedIndexableFields = {};
Object.values( indexableFields ).map( ( field ) => {
cleanedIndexableFields[ field.id ] = {
id: field.id,
label: field.label,
type: field.type,
searchable: field.searchable,
filterable: field.filterable,
sortable: field.sortable,
};
} );
//set global to reuse in other components
findstr.settingsIndexableFields = cleanedIndexableFields;
setIsLoading( true );
apiFetch( {
path: 'findstr/v1/indexable-fields',
method: 'POST',
data: {
indexableFields: cleanedIndexableFields,
},
} ).then( ( data ) => {
setIsLoading( false );
} );
}
//prepare fields for output in repeater
const fieldsPrepareOutput = function ( fields ) {
for ( const [ key, value ] of Object.entries( fields ) ) {
fields[ key ]._canSort = ! key.includes( 'tax/' );
fields[ key ]._canDelete = ! unDeletableFields.includes( key );
fields[ key ]._canEditFilterable =
! fieldsDisabledFilterableEdit.includes( key );
}
return fields;
};
/**
* Update indexable fields _canSort property and _canDelete property
*/
useEffect( () => {
updateIndexableField();
}, [ indexableFields ] );
/**
* handle Sort
* @param fields
*/
const handleSort = ( fields ) => {
const tempIndexableFields = {};
fields.forEach( ( field, index ) => {
tempIndexableFields[ field.id ] = field;
} );
setIndexableFields( {
...tempIndexableFields,
} );
};
/**
* Handle scroll event to fix the header
*/
useEffect( () => {
const header = document.getElementById( 'fields-header' );
const headerWidth = header.getBoundingClientRect().width;
const table = document.querySelector( '.fields' );
const adminBarHeight =
document.getElementById( 'wpadminbar' )?.clientHeight;
const handleScroll = () => {
const rect = table.getBoundingClientRect();
if ( rect.top <= adminBarHeight ) {
header.classList.add( 'fixed-header' );
header.style.width = headerWidth + 'px';
header.style.top = adminBarHeight + 'px';
} else {
header.classList.remove( 'fixed-header' );
header.style.width = '';
}
};
window.addEventListener( 'scroll', handleScroll );
// Cleanup function to remove the event listener when the component unmounts
return () => {
window.removeEventListener( 'scroll', handleScroll );
};
}, [] );
return (
<>
{ isAddFieldFormOpen && (
<>
<AddIndexableField
isOpen={ isAddFieldFormOpen }
setIsOpen={ openAddFieldForm }
indexableFields={ indexableFields }
setIndexableFields={ setIndexableFields }
/>
</>
) }
{ fieldToDelete && (
<>
<ConfirmDialog
isConfirmDialogOpen={ fieldToDelete }
setIsConfirmDialogOpen={ setFieldToDelete }
message={ __(
'Are you sure you want to delete this field?',
'findstr'
) }
title={ __( 'Delete indexable field', 'findstr' ) }
onConfirm={ () => {
delete indexableFields[ fieldToDelete.id ];
setIndexableFields( {
...indexableFields,
} );
setFieldToDelete( null );
} }
isLoading={ isLoading }
/>
</>
) }
<div id="indexable-fields">
<h3>
<label>{ __( 'Fields to index', 'findstr' ) }</label>
</h3>
<div className="fields table-flex">
<div
className="table-row table-header fields-header"
id="fields-header"
key="fields-header"
>
<div className="manage">
{ isLoading && <Spinner /> }
</div>
<div className="field">
{ _x( 'Field', 'index settings', 'findstr' ) }
</div>
<div className="type">
{ _x( 'Type', 'index settings', 'findstr' ) }
</div>
<div className="searchable">
{ _x( 'Searchable', 'index settings', 'findstr' ) }
</div>
<div className="filterable">
{ _x( 'Filterable', 'index settings', 'findstr' ) }
</div>
<div className="sortable">
{ _x( 'Sortable', 'index settings', 'findstr' ) }
</div>
</div>
<Repeater
items={ Object.values(
fieldsPrepareOutput( indexableFields )
) }
onOrderChange={ ( fields ) => {
handleSort( fields );
} }
addButton={ () => {
return (
<Button
isSecondary
onClick={ () => {
openAddFieldForm( true );
} }
>
{ _x( 'Add new', 'fields', 'findstr' ) }
</Button>
);
} }
>
{ ( field, key ) => (
<div
className="table-row fields-list"
id={ field.id }
key={ field.id }
>
<div className="field">
{ field.label }
<div className="row-actions">
{ field._canDelete && (
<a
onClick={ ( e ) => {
deleteIndexableField(
field
);
e.preventDefault();
} }
href="#"
>
{ _x(
'Delete',
'fields',
'findstr'
) }
</a>
) }
</div>
</div>
<div className="type">
<div className="select-wrapper">
<SelectControl
__nextHasNoMarginBottom={ true }
options={ fieldTypes }
value={ field.type || 'text' }
onChange={ ( value ) => {
field.type = value;
indexableFields[ field.id ] =
field;
setIndexableFields( {
...indexableFields,
} );
} }
/>
</div>
</div>
<div className="searchable">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ field.searchable }
onChange={ ( value ) => {
field.searchable = value;
indexableFields[ field.id ] = field;
setIndexableFields( {
...indexableFields,
} );
} }
/>
</div>
<div className="filterable">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ field.filterable }
disabled={ ! field._canEditFilterable }
onChange={ ( value ) => {
field.filterable = value;
indexableFields[ field.id ] = field;
setIndexableFields( {
...indexableFields,
} );
} }
/>
</div>
<div className="sortable">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ field.sortable }
disabled={ ! field._canSort }
onChange={ ( value ) => {
field.sortable = value;
indexableFields[ field.id ] = field;
setIndexableFields( {
...indexableFields,
} );
} }
/>
</div>
</div>
) }
</Repeater>
</div>
</div>
</>
);
}