File "render-helper.js"

Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/src/apps/admin-components/schema-utils/render-helper.js
File size: 18.17 KB
MIME-type: text/x-java
Charset: utf-8

import { __ } from '@wordpress/i18n';
import { Select, EditorInput, Button, Label, Input, Text } from '@bsf/force-ui';
import { editorValueToString, stringValueToFormatJSON } from '@Functions/utils';
import { Trash, Plus, Info } from 'lucide-react';
import { generateUUID } from '@AdminComponents/schema-utils/utils';
import { SeoPopupTooltip } from '@AdminComponents/tooltip';

const WORD_BREAK_ALL_EDITOR_INPUT = [ 'url', 'logo' ];
const STYLES_OVERRIDE_FOR_EDITOR_INPUT = {
	wordBreak: 'break-all',
};

// Common function to render cloneable group fields with stable ID management
export const renderCloneableGroupField = ( {
	field,
	schemaId,
	getFieldValue,
	onFieldChange,
	variableSuggestions,
	fieldItemIds,
	setFieldItemIds,
	renderHelpTextFunction = null,
} ) => {
	let existingValues = getFieldValue( field.id ) || [];

	// Ensure existingValues is always an array
	if ( ! Array.isArray( existingValues ) ) {
		if ( typeof existingValues === 'object' && existingValues !== null ) {
			existingValues = Object.values( existingValues );
		} else {
			existingValues = [];
		}
	}

	// Ensure at least one empty item exists
	if ( existingValues.length === 0 ) {
		const defaultItem = {};
		field.fields.forEach( ( subField ) => {
			if ( subField.type === 'Group' && subField.fields ) {
				const nestedGroup = {};
				subField.fields.forEach( ( nestedField ) => {
					nestedGroup[ nestedField.id ] = nestedField.std || '';
				} );
				defaultItem[ subField.id ] = nestedGroup;
			} else {
				defaultItem[ subField.id ] = subField.std || '';
			}
		} );
		existingValues = [ defaultItem ];
	}

	// Ensure all nested groups have their required fields (like @type)
	existingValues = existingValues.map( ( item ) => {
		const updatedItem = { ...item };
		field.fields.forEach( ( subField ) => {
			if ( subField.type === 'Group' && subField.fields ) {
				// Make sure the nested group exists
				if (
					! updatedItem[ subField.id ] ||
					typeof updatedItem[ subField.id ] !== 'object'
				) {
					updatedItem[ subField.id ] = {};
				}

				// Ensure all required fields exist in the nested group
				subField.fields.forEach( ( nestedField ) => {
					if (
						nestedField.required &&
						updatedItem[ subField.id ][ nestedField.id ] ===
							undefined
					) {
						updatedItem[ subField.id ][ nestedField.id ] =
							nestedField.std || '';
					}
				} );
			}
		} );
		return updatedItem;
	} );

	// Generate stable IDs for this field's items
	const fieldKey = `${ schemaId }-${ field.id }`;
	if (
		! fieldItemIds[ fieldKey ] ||
		fieldItemIds[ fieldKey ].length !== existingValues.length
	) {
		const newIds = existingValues.map(
			( _, index ) =>
				fieldItemIds[ fieldKey ]?.[ index ] ||
				`item-${ Date.now() }-${ index }-${ Math.random()
					.toString( 36 )
					.substr( 2, 9 ) }`
		);
		setFieldItemIds( ( prev ) => ( {
			...prev,
			[ fieldKey ]: newIds,
		} ) );
	}

	const currentIds = fieldItemIds[ fieldKey ] || [];
	const itemsWithIds = existingValues.map( ( item, index ) => ( {
		...item,
		_id: currentIds[ index ] || `temp-${ index }`,
	} ) );

	const handleAddNewItem = () => {
		const newItem = {};
		field.fields.forEach( ( subField ) => {
			if ( subField.type === 'Group' && subField.fields ) {
				const nestedGroup = {};
				subField.fields.forEach( ( nestedField ) => {
					nestedGroup[ nestedField.id ] = nestedField.std || '';
				} );
				newItem[ subField.id ] = nestedGroup;
			} else {
				newItem[ subField.id ] = subField.std || '';
			}
		} );

		const updatedValues = [ ...existingValues, newItem ];
		const newId = `item-${ Date.now() }-${
			existingValues.length
		}-${ Math.random().toString( 36 ).substr( 2, 9 ) }`;

		setFieldItemIds( ( prev ) => ( {
			...prev,
			[ fieldKey ]: [ ...( prev[ fieldKey ] || [] ), newId ],
		} ) );

		onFieldChange( field.id, updatedValues );
	};

	const handleRemoveItem = ( index ) => {
		const updatedValues = existingValues.filter( ( _, i ) => i !== index );
		const updatedIds = currentIds.filter( ( _, i ) => i !== index );

		setFieldItemIds( ( prev ) => ( {
			...prev,
			[ fieldKey ]: updatedIds,
		} ) );

		onFieldChange( field.id, updatedValues );
	};

	const handleItemFieldChange = ( itemIndex, fieldId, value ) => {
		const updatedValues = [ ...existingValues ];
		updatedValues[ itemIndex ] = {
			...updatedValues[ itemIndex ],
			[ fieldId ]: value,
		};
		onFieldChange( field.id, updatedValues );
	};

	return (
		<>
			{ itemsWithIds.map( ( item, index ) => (
				<div
					key={ item._id }
					className="border border-gray-200 rounded-lg mb-4 space-y-3"
				>
					<div className="flex items-center justify-between">
						<Text
							size={ 14 }
							lineHeight={ 20 }
							weight={ 500 }
							className="text-text-primary"
						>
							{ field.cloneItemHeading || `Item ${ index + 1 }` }
						</Text>
						{ itemsWithIds.length > 1 && (
							<Button
								variant="ghost"
								size="sm"
								onClick={ () => handleRemoveItem( index ) }
								icon={
									<Trash
										strokeWidth={ 1.5 }
										className="text-icon-secondary"
									/>
								}
							/>
						) }
					</div>

					{ field.fields.map( ( subField ) => {
						if ( subField.hidden || subField.type === 'Hidden' ) {
							return null;
						}

						// Handle nested Group fields
						if ( subField.type === 'Group' && subField.fields ) {
							return (
								<div
									key={ subField.id }
									className="space-y-1.5"
								>
									<div className="flex items-center justify-start gap-1.5 w-full">
										<Label
											tag="span"
											size="sm"
											className="space-x-0.5"
											required={ subField.required }
										>
											{ subField.label }
										</Label>
										{ subField.tooltip && (
											<SeoPopupTooltip
												content={ subField.tooltip }
												placement="top"
												arrow
												className="z-[99999]"
											>
												<Info
													className="size-4 text-icon-secondary"
													title={ subField.tooltip }
												/>
											</SeoPopupTooltip>
										) }
									</div>
									{ subField.fields.map( ( nestedField ) => {
										if (
											nestedField.hidden ||
											nestedField.type === 'Hidden'
										) {
											return null;
										}

										return (
											<div
												key={ nestedField.id }
												className="space-y-1.5"
											>
												<div className="flex items-center justify-start gap-1.5 w-full">
													<Label
														tag="span"
														size="sm"
														className="space-x-0.5"
														required={
															nestedField.required
														}
													>
														{ nestedField.label }
													</Label>
													{ nestedField.tooltip && (
														<SeoPopupTooltip
															content={
																nestedField.tooltip
															}
															placement="top"
															arrow
															className="z-[99999]"
														>
															<Info
																className="size-4 text-icon-secondary"
																title={
																	nestedField.tooltip
																}
															/>
														</SeoPopupTooltip>
													) }
												</div>
												<div className="flex items-center justify-start gap-1.5 w-full">
													{ renderFieldCommon( {
														field: {
															...nestedField,
															id: nestedField.id,
														},
														getFieldValue: () => {
															const groupValue =
																item[
																	subField.id
																] || {};
															return (
																groupValue[
																	nestedField
																		.id
																] ||
																nestedField.std ||
																''
															);
														},
														onFieldChange: (
															fieldId,
															value
														) => {
															const currentGroupValue =
																item[
																	subField.id
																] || {};
															const updatedGroupValue =
																{
																	...currentGroupValue,
																	[ fieldId ]:
																		value,
																};
															handleItemFieldChange(
																index,
																subField.id,
																updatedGroupValue
															);
														},
														variableSuggestions,
														renderAsGroupComponent: false,
													} ) }
												</div>
												{ renderHelpTextFunction &&
													renderHelpTextFunction(
														nestedField
													) }
											</div>
										);
									} ) }
									{ renderHelpTextFunction &&
										renderHelpTextFunction( subField ) }
								</div>
							);
						}

						return (
							<div key={ subField.id } className="space-y-1.5">
								<div className="flex items-center justify-start gap-1.5 w-full">
									<Label
										tag="span"
										size="sm"
										className="space-x-0.5"
										required={ subField.required }
									>
										{ subField.label }
									</Label>
									{ subField.tooltip && (
										<SeoPopupTooltip
											content={ subField.tooltip }
											placement="top"
											arrow
											className="z-[99999]"
										>
											<Info
												className="size-4 text-icon-secondary"
												title={ subField.tooltip }
											/>
										</SeoPopupTooltip>
									) }
								</div>
								<div className="flex items-center justify-start gap-1.5 w-full">
									{ renderFieldCommon( {
										field: {
											...subField,
											id: subField.id,
										},
										getFieldValue: () =>
											item[ subField.id ] ||
											subField.std ||
											'',
										onFieldChange: ( fieldId, value ) =>
											handleItemFieldChange(
												index,
												fieldId,
												value
											),
										variableSuggestions,
										renderAsGroupComponent: false,
									} ) }
								</div>
								{ renderHelpTextFunction &&
									renderHelpTextFunction( subField ) }
							</div>
						);
					} ) }
				</div>
			) ) }

			<Button
				variant="outline"
				className="w-fit"
				size="sm"
				onClick={ handleAddNewItem }
				icon={ <Plus /> }
			>
				{ __( 'Add New', 'surerank' ) }
			</Button>
		</>
	);
};

// Add the GroupFieldRenderer component
export const GroupFieldRenderer = ( {
	field,
	schemaType,
	getFieldValue,
	onFieldChange,
	variableSuggestions,
} ) => {
	if ( ! field.fields || field.fields.length === 0 ) {
		return null;
	}

	const groupType = field.fields.find( ( f ) => f.id === '@type' )
		? getFieldValue( '@type', field.id )
		: null;

	return (
		<div className="space-y-2 w-full border-l-2 border-gray-100 pt-2">
			{ field.fields.map( ( subField ) => {
				if ( subField.hidden || subField.type === 'Hidden' ) {
					return null;
				}

				if (
					subField.main &&
					groupType &&
					subField.main !== groupType
				) {
					return null;
				}

				return (
					<div key={ subField.id } className="space-y-1.5">
						<div className="flex items-center justify-start gap-1.5 w-full">
							<Label
								tag="span"
								size="sm"
								className="space-x-0.5"
								required={ subField.required }
							>
								<span>{ subField.label }</span>
							</Label>
							{ subField.tooltip && (
								<SeoPopupTooltip
									content={ subField.tooltip }
									placement="top"
									arrow
									className="z-[99999]"
								>
									<Info
										className="size-4 text-icon-secondary"
										title={ subField.tooltip }
									/>
								</SeoPopupTooltip>
							) }
						</div>
						<div className="flex items-center justify-start gap-1.5 w-full">
							{ renderFieldCommon( {
								field: subField,
								schemaType,
								getFieldValue: ( fieldId ) =>
									getFieldValue( fieldId, field.id ),
								onFieldChange: ( fieldId, value ) =>
									onFieldChange( fieldId, value, field.id ),
								variableSuggestions,
								renderAsGroupComponent: false,
							} ) }
						</div>
						{ subField.type !== 'Select' && (
							<Text size={ 14 } weight={ 400 } color="help">
								{ __(
									'Type @ to view variable suggestions',
									'surerank'
								) }
							</Text>
						) }
					</div>
				);
			} ) }
		</div>
	);
};

export const renderCloneableField = ( {
	field,
	getFieldValue,
	onFieldChange,
	variableSuggestions,
	placeholder = '',
} ) => {
	const existingValues = getFieldValue( field.id ) || {};

	if ( Object.keys( existingValues ).length === 0 ) {
		existingValues[ generateUUID( 7 ) ] = ''; // Ensure first key is unique
	}

	const handleAddNewField = () => {
		const newId = generateUUID( 7 );
		const updatedValues = {
			...existingValues,
			[ newId ]: '',
		};
		onFieldChange( field.id, updatedValues );
	};

	return (
		<div className="flex flex-col gap-2 w-full">
			{ Object.entries( existingValues ).map( ( [ key, value ] ) => (
				<div key={ key } className="flex items-center gap-1.5 w-full">
					<EditorInput
						by="label"
						trigger="@"
						options={ variableSuggestions }
						placeholder={ placeholder }
						defaultValue={ stringValueToFormatJSON(
							value,
							variableSuggestions,
							'value'
						) }
						onChange={ ( editorState ) => {
							onFieldChange( field.id, {
								...existingValues,
								[ key ]: editorValueToString(
									editorState.toJSON()
								),
							} );
						} }
					/>
					<Button
						variant="ghost"
						size="md"
						onClick={ () => {
							const updatedValues = { ...existingValues };
							delete updatedValues[ key ]; // Remove entry
							onFieldChange( field.id, updatedValues );
						} }
						icon={
							<Trash
								strokeWidth={ 1.5 }
								className="text-icon-secondary"
							/>
						}
					/>
				</div>
			) ) }
			<Button
				variant="outline"
				className="w-fit"
				size="sm"
				onClick={ handleAddNewField }
				icon={ <Plus /> }
			>
				{ __( 'Add New', 'surerank' ) }
			</Button>
		</div>
	);
};

export function renderFieldCommon( {
	field,
	getFieldValue,
	onFieldChange,
	variableSuggestions,
	placeholder = '',
	renderAsGroupComponent = false,
} ) {
	if ( ! field ) {
		return null;
	}

	const currentFieldValue = getFieldValue( field.id ) || field.std || '';

	switch ( field.type ) {
		case 'Select': {
			const options = Array.isArray( field.options )
				? field.options.reduce( ( acc, group ) => {
						if ( group.options ) {
							return { ...acc, ...group.options };
						}
						return acc;
				  }, {} )
				: field.options || {};

			return (
				<div key={ field.id } className="w-full">
					<Select
						size="md"
						value={ currentFieldValue }
						onChange={ ( value ) =>
							onFieldChange( field.id, value )
						}
					>
						<Select.Button />
						<Select.Options className="z-50">
							{ Object.entries( options ).map(
								( [ key, label ] ) => (
									<Select.Option key={ key } value={ key }>
										{ label }
									</Select.Option>
								)
							) }
						</Select.Options>
					</Select>
				</div>
			);
		}

		case 'Group': {
			if ( renderAsGroupComponent ) {
				return (
					<GroupFieldRenderer
						key={ field.id }
						field={ field }
						getFieldValue={ getFieldValue }
						onFieldChange={ onFieldChange }
						variableSuggestions={ variableSuggestions }
					/>
				);
			}

			if ( ! field.fields?.length ) {
				return null;
			}

			return (
				<div key={ field.id } className="space-y-2 w-full">
					<div className="space-y-4 pl-4">
						{ field.fields.map(
							( subField ) =>
								! subField.hidden &&
								subField.type !== 'Hidden' && (
									<div
										key={ subField.id }
										className="flex items-center gap-4"
									>
										{ /* Label, etc. */ }
										{ /* (You could even recursively call renderFieldCommon for subField here) */ }
									</div>
								)
						) }
					</div>
				</div>
			);
		}

		case 'SelectGroup': {
			const groupOptions = Object.values( field?.options || {} );
			return (
				<div key={ field.id } className="w-full">
					<Select
						size="md"
						value={ currentFieldValue }
						onChange={ ( value ) =>
							onFieldChange( field.id, value )
						}
						combobox
						placeholder={ __(
							'Search or select an option',
							'surerank'
						) }
						aria-label={ field.label }
					>
						<Select.Button
							placeholder={ __(
								'Search or select an option',
								'surerank'
							) }
						/>
						<Select.Options>
							{ groupOptions.map( ( group, index ) => (
								<Select.OptionGroup
									key={ index }
									label={ group.label }
								>
									{ Object.entries( group.options ).map(
										( [ key, label ] ) => (
											<Select.Option
												key={ key }
												value={ key }
											>
												{ label }
											</Select.Option>
										)
									) }
								</Select.OptionGroup>
							) ) }
						</Select.Options>
					</Select>
				</div>
			);
		}

		case 'Title': {
			return (
				<div className="w-full">
					<Input
						key={ field.id }
						by="label"
						placeholder={ placeholder }
						defaultValue={ currentFieldValue }
						aria-label={ field.label }
						className="flex-grow max-w-full mdx"
						size="md"
						type="text"
						onChange={ ( value ) => {
							onFieldChange( field.id, value );
						} }
					/>
				</div>
			);
		}

		default:
			return (
				<EditorInput
					key={ field.id }
					by="label"
					trigger="@"
					options={ variableSuggestions }
					placeholder={ placeholder }
					defaultValue={ stringValueToFormatJSON(
						currentFieldValue,
						variableSuggestions,
						'value'
					) }
					onChange={ ( editorState ) => {
						onFieldChange(
							field.id,
							editorValueToString( editorState.toJSON() )
						);
					} }
					className="flex-grow"
					wrapperClassName="[&>ul>li]:capitalize"
					{ ...( WORD_BREAK_ALL_EDITOR_INPUT.includes( field.id ) && {
						style: STYLES_OVERRIDE_FOR_EDITOR_INPUT,
					} ) }
				/>
			);
	}
}

export function renderHelpText( field ) {
	if (
		field?.type === 'Group' ||
		field?.type === 'Select' ||
		field?.type === 'SelectGroup' ||
		field?.id === 'schema_name'
	) {
		return null;
	}

	return (
		/**
		 * @description Help text not shown for schema_name, Group, Select, and SelectGroup fields
		 */
		<Text size={ 14 } weight={ 400 } color="help">
			{ __( 'Type @ to view variable suggestions', 'surerank' ) }
		</Text>
	);
}