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