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