FileMaster
Search
Toggle Dark Mode
Home
/
.
/
wp-content
/
plugins
/
surerank
/
src
/
apps
/
seo-popup
/
components
/
meta-settings
/
tab-content
Edit File: schema.js
import { __ } from '@wordpress/i18n'; import { useEffect, useMemo, useState } from '@wordpress/element'; import { Label, Alert, Accordion, Text } from '@bsf/force-ui'; import { Info, Trash } from 'lucide-react'; import { applyFilters } from '@wordpress/hooks'; import WpSchemaProNotice from '@/global/components/wp-schema-pro-notice'; import { cn } from '@Functions/utils'; import { renderFieldCommon, renderFieldSwitch, } from '@AdminComponents/schema-utils/render-helper'; import { AddFieldMenu } from '@AdminComponents/schema-utils/add-field-menu'; import { DeleteFieldButton } from '@AdminComponents/schema-utils/delete-field-button'; import { noFieldsAlert, generateUUID, isSchemaTypeValid, processFields, shouldShowField, getAvailableFields, canDeleteField, sortFieldsByPriority, } from '@AdminComponents/schema-utils/utils'; import Modal from '@/apps/admin-general/schema/modal'; import { SeoPopupTooltip } from '@AdminComponents/tooltip'; import ConfirmationPopover from '@/global/components/confirmation-popover'; const SchemaTab = ( { postMetaData, globalDefaults, updatePostMetaData } ) => { const closeModal = () => setIsModalOpen( false ); const [ isModalOpen, setIsModalOpen ] = useState( false ); const [ selectedSchema, setSelectedSchema ] = useState( '' ); const [ selectedType, setSelectedType ] = useState( '' ); const [ expandedSchemaId, setExpandedSchemaId ] = useState( null ); const [ fieldItemIds, setFieldItemIds ] = useState( {} ); const isWpSchemaProActive = surerank_globals?.wp_schema_pro_active || false; const defaultSchemasObject = surerank_globals?.default_schemas || {}; const defaultSchemas = Object.entries( defaultSchemasObject ).map( ( [ id, schema ] ) => ( { id, ...schema, } ) ); const globalSchemas = globalDefaults.schemas || {}; const schemas = postMetaData.schemas ? postMetaData.schemas : globalSchemas || {}; const validSchemas = useMemo( () => Object.entries( schemas ).filter( ( [ , schema ] ) => { return isSchemaTypeValid( schema?.title ); } ), [ schemas ] ); const schemaTypeData = surerank_globals?.schema_type_data || {}; const schemaTypeOptions = surerank_globals?.schema_type_options || {}; const variableSuggestions = useMemo( () => Object.entries( surerank_globals?.schema_variables || {} ).map( ( [ value, label ] ) => ( { value, label, } ) ), [] ); const handleAddField = ( schemaId, fieldId ) => { const schemaTitle = schemas[ schemaId ]?.title; const allFields = schemaTypeData[ schemaTitle ] || []; const fieldToAdd = allFields.find( ( f ) => f.id === fieldId ); if ( ! fieldToAdd ) { return; } let defaultValue = fieldToAdd.std !== undefined ? fieldToAdd.std : ''; if ( fieldToAdd.type === 'Group' && fieldToAdd.fields ) { defaultValue = processFields( [ fieldToAdd ] )[ fieldToAdd.id ]; } const updatedSchemas = { ...schemas, [ schemaId ]: { ...schemas[ schemaId ], fields: { ...schemas[ schemaId ].fields, [ fieldId ]: defaultValue, }, }, }; const cleanedSchemas = cleanSchemas( updatedSchemas ); updatePostMetaData( { schemas: cleanedSchemas, } ); }; const handleDeleteField = ( schemaId, fieldId ) => { const updatedFields = { ...schemas[ schemaId ].fields }; delete updatedFields[ fieldId ]; const updatedSchemas = { ...schemas, [ schemaId ]: { ...schemas[ schemaId ], fields: updatedFields, }, }; const cleanedSchemas = cleanSchemas( updatedSchemas ); updatePostMetaData( { schemas: cleanedSchemas, } ); }; const cleanSchemas = ( schemasData ) => { const cleanedSchemas = {}; Object.entries( schemasData ).forEach( ( [ schemaId, schema ] ) => { const { show_on, not_show_on, ...rest } = schema; // we removed show_on and not_show_on, as we are using post meta data for schema now, cause we edited now. const cleanedSchema = { ...rest, parent: true, }; cleanedSchemas[ schemaId ] = cleanedSchema; } ); return cleanedSchemas; }; useEffect( () => { const updatedSchemas = {}; Object.entries( schemas ).forEach( ( [ schemaId, schema ] ) => { const schemaFields = schemaTypeData[ schema.title ] || []; const existingFields = schema.fields || {}; // Only initialize with default=true or required=true fields const defaultFields = processFields( schemaFields, true ); const mergedFields = { ...defaultFields, ...existingFields }; if ( Object.keys( existingFields ).length === 0 ) { mergedFields[ '@type' ] = schema?.type || ''; updatedSchemas[ schemaId ] = { ...schema, fields: mergedFields, }; } } ); if ( Object.keys( updatedSchemas ).length > 0 ) { const cleanedSchemas = cleanSchemas( { ...schemas, ...updatedSchemas, } ); if ( JSON.stringify( cleanedSchemas ) !== JSON.stringify( schemas ) ) { updatePostMetaData( { schemas: cleanedSchemas, } ); } } }, [ schemas, schemaTypeData, updatePostMetaData ] ); const handleDeleteSchema = ( schemaId ) => { const updatedSchemas = { ...schemas }; delete updatedSchemas[ schemaId ]; const cleanedSchemas = cleanSchemas( updatedSchemas ); updatePostMetaData( { schemas: cleanedSchemas, } ); }; const handleAddSchema = () => { const schemaUniqueId = generateUUID(); const newSchema = { title: selectedSchema, type: selectedType, show_on: { rules: [], specific: [], specificText: [], }, fields: {}, }; const updatedSchemas = { ...schemas, [ schemaUniqueId ]: newSchema }; const cleanedSchemas = cleanSchemas( updatedSchemas ); updatePostMetaData( { schemas: cleanedSchemas, } ); setExpandedSchemaId( schemaUniqueId ); setIsModalOpen( false ); setSelectedSchema( '' ); setSelectedType( '' ); }; const handleFieldUpdate = ( schemaId, fieldKey, newValue ) => { const updatedSchemas = { ...schemas, [ schemaId ]: { ...schemas[ schemaId ], fields: { ...schemas[ schemaId ].fields, [ fieldKey ]: newValue, }, }, }; const cleanedSchemas = cleanSchemas( updatedSchemas ); updatePostMetaData( { schemas: cleanedSchemas, } ); }; const getFieldValue = ( schemaId, fieldId ) => { return schemas[ schemaId ]?.fields?.[ fieldId ] || ''; }; const onFieldChange = ( schemaId, fieldId, newValue ) => { handleFieldUpdate( schemaId, fieldId, newValue ); if ( fieldId === '@type' ) { const updatedSchemas = { ...schemas }; updatedSchemas[ schemaId ].type = updatedSchemas[ schemaId ]?.title || ''; // added for backward compatibility. updatedSchemas[ schemaId ].fields[ '@type' ] = newValue; updatePostMetaData( { schemas: updatedSchemas, } ); } }; /** * Render a single subfield within a non-cloneable Group field * * @param {Object} field - The parent Group field configuration * @param {Object} subField - The subfield to render * @param {string} schemaId - The schema ID * @return {JSX.Element|null} - Rendered subfield or null if hidden */ const renderGroupSubField = ( field, subField, schemaId ) => { if ( subField.type === 'Hidden' ) { return null; } const groupValue = getFieldValue( schemaId, field.id ) || {}; const handleSubFieldChange = ( fldId, newVal ) => { const currentGroupValue = getFieldValue( schemaId, field.id ) || {}; const updatedGroupValue = { ...currentGroupValue, [ fldId ]: newVal, }; onFieldChange( schemaId, field.id, updatedGroupValue ); }; return ( <div key={ subField.id } className="flex flex-col items-start justify-start gap-1.5 w-full" > <div className="flex items-center gap-1.5"> <Label tag="span" size="sm"> { 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 gap-1.5 w-full"> { renderFieldCommon( { field: subField, schemaType: schemas[ schemaId ].type, getFieldValue: ( fldId ) => groupValue[ fldId ] || subField.std || '', onFieldChange: handleSubFieldChange, variableSuggestions, renderAsGroupComponent: false, } ) } </div> </div> ); }; /** * Render a field based on its type and configuration * * @param {Object} field - The field configuration * @param {string} schemaId - The schema ID * @return {JSX.Element} - Rendered field component */ const renderField = ( field, schemaId ) => { if ( field.type === 'Group' && ! field.cloneable ) { return ( <div className="flex flex-col w-full space-y-3 border border-border-subtle rounded-lg p-3"> { field.fields.map( ( subField ) => renderGroupSubField( field, subField, schemaId ) ) } </div> ); } return renderFieldSwitch( field, { schemaId, schemaType: schemas[ schemaId ].type, getFieldValue: ( fieldId ) => getFieldValue( schemaId, fieldId ), onFieldChange: ( fieldId, newVal ) => onFieldChange( schemaId, fieldId, newVal ), variableSuggestions, fieldItemIds, setFieldItemIds, renderAsGroupComponent: true, } ); }; const renderSchemaFields = ( schemaId ) => { const schemaTitle = schemas[ schemaId ]?.title; // Check if schema type is valid first if ( ! isSchemaTypeValid( schemaTitle ) ) { return noFieldsAlert; } const allSchemaFields = schemaTypeData[ schemaTitle ] || []; const existingFields = schemas[ schemaId ]?.fields || {}; if ( allSchemaFields.length === 0 || allSchemaFields.every( ( field ) => field.type === 'Hidden' || field.id === 'schema_name' ) ) { return noFieldsAlert; } // Filter fields to show: must exist in existingFields OR be required OR have parent dependency const filteredFields = allSchemaFields.filter( ( field ) => { if ( field.type === 'Hidden' || field.type === 'SchemaDocs' ) { return false; } // Show if field exists in the schema if ( Object.prototype.hasOwnProperty.call( existingFields, field.id ) ) { return true; } // Show if required if ( field.required ) { return true; } // Show if has parent dependency that is active if ( field.parent && field.parent_option ) { return shouldShowField( field, ( fieldId ) => getFieldValue( schemaId, fieldId ) ); } return false; } ); const fieldsToShow = sortFieldsByPriority( filteredFields, existingFields ); const availableFields = getAvailableFields( allSchemaFields, existingFields ); return ( <> { fieldsToShow.map( ( field ) => { if ( field.parent && field.parent_option ) { const shouldShow = shouldShowField( field, ( fieldId ) => getFieldValue( schemaId, fieldId ) ); if ( ! shouldShow ) { return null; } } const canDelete = canDeleteField( field ); return ( <div key={ field.id } className="flex flex-col items-start justify-start gap-1.5 w-full p-1" > { /* Label + tooltip + delete button */ } <div className="flex items-center justify-between gap-1.5 w-full"> <div className="flex items-center gap-1.5"> <Label tag="span" size="sm" > { field.label } </Label> { field.tooltip && ( <SeoPopupTooltip content={ field.tooltip } placement="top" arrow className="z-[99999]" > <Info className="size-4 text-icon-secondary" title={ field.tooltip } /> </SeoPopupTooltip> ) } </div> { canDelete && ( <DeleteFieldButton onDelete={ () => handleDeleteField( schemaId, field.id ) } /> ) } </div> { /* Field render */ } { renderField( field, schemaId ) } </div> ); } ) } { applyFilters( 'surerank.schema.properties.extensions', null, { schemaId, schemaType: schemas[ schemaId ]?.type || schemaTitle, schema: schemaTitle, metaSettings: { schemas }, currentSchema: schemas[ schemaId ] || {}, setMetaSetting: ( key, value ) => { if ( key === 'schemas' ) { updatePostMetaData( { schemas: value } ); } }, variableSuggestions, getFieldValue: ( fieldId ) => getFieldValue( schemaId, fieldId ), onFieldChange: ( fieldId, newValue ) => onFieldChange( schemaId, fieldId, newValue ), } ) } <AddFieldMenu availableFields={ availableFields } onAddField={ ( fieldId ) => handleAddField( schemaId, fieldId ) } className="p-2 w-full border-t border-border-subtle mt-2" filterContext={ { schemaId, schemaType: schemas[ schemaId ]?.type || schemaTitle, schema: schemaTitle, metaSettings: { schemas }, currentSchema: schemas[ schemaId ] || {}, setMetaSetting: ( key, value ) => { if ( key === 'schemas' ) { updatePostMetaData( { schemas: value } ); } }, } } /> </> ); }; if ( isWpSchemaProActive ) { return <WpSchemaProNotice />; } return ( <div className="pt-2 gap-2"> <div className="flex items-center mb-4.5 -mt-0.5"> <Text size={ 14 } weight={ 500 } color="label"> { __( 'Schemas in Use', 'surerank' ) } </Text> </div> <div className={ cn( 'w-full bg-background-secondary flex flex-col items-center justify-center rounded p-1' ) } > { validSchemas.length > 0 ? ( <Accordion type="simple" iconType="arrow" className="w-full space-y-1" autoClose={ false } > { validSchemas.map( ( [ schemaId, schema ] ) => { return ( <Accordion.Item key={ schemaId } value={ schemaId } className="bg-background-primary rounded-md" defaultExpanded={ schemaId === expandedSchemaId } > <Accordion.Trigger iconType="arrow" className="hover:bg-background-primary rounded-md flex justify-between items-center [&>div]:w-full p-2 gap-2 [&>svg]:size-4 cursor-pointer" > <span className="text-base font-normal text-text-primary leading-6 ml-1"> { schema.title } </span> <ConfirmationPopover onConfirm={ () => handleDeleteSchema( schemaId ) } placement="bottom" offset={ { mainAxis: 8, crossAxis: -28, } } > <div className="inline-flex ml-auto" role="button" tabIndex={ 0 } > <Trash className="size-3.5 text-icon-secondary cursor-pointer" /> </div> </ConfirmationPopover> </Accordion.Trigger> <Accordion.Content> <div className="mt-3 space-y-4"> { renderSchemaFields( schemaId ) } </div> </Accordion.Content> </Accordion.Item> ); } ) } </Accordion> ) : ( <Alert className="w-full shadow-none" content={ __( 'No schemas configured.', 'surerank' ) } variant="info" /> ) } </div> <div className="w-full mt-6 rounded"> <Modal selectedSchema={ selectedSchema } setSelectedSchema={ setSelectedSchema } selectedType={ selectedType } setSelectedType={ setSelectedType } schemaTypeOptions={ schemaTypeOptions } defaultSchemas={ defaultSchemas } handleAddSchema={ handleAddSchema } isModalOpen={ isModalOpen } closeModal={ closeModal } /> </div> </div> ); }; export default SchemaTab;
Save
Back