Skip to content

Implement repeatable array field support#37

Merged
erseco merged 1 commit intomainfrom
feature/add-array-fields-for-annexes-support-cmdtk9
Oct 14, 2025
Merged

Implement repeatable array field support#37
erseco merged 1 commit intomainfrom
feature/add-array-fields-for-annexes-support-cmdtk9

Conversation

@erseco
Copy link
Copy Markdown
Collaborator

@erseco erseco commented Oct 14, 2025

Summary

  • extend the template parser and document-type admin workflow to detect array placeholders and expose item schemas
  • replace the sections metabox with repeatable array controls that store JSON in structured content while previews and export merges consume decoded arrays
  • rebuild the annexes admin script as a vanilla JS repeater supporting add/remove/reorder operations

Testing

  • composer test (fails: missing WordPress native test bootstrap)
  • ./vendor/bin/phpcs --standard=.phpcs.xml.dist .

https://chatgpt.com/codex/tasks/task_e_68ee30cf252083229149dd9f656091e1

Comment on lines +291 to +372
private function build_schema_from_fields( $fields ) {
$raw_schema = Resolate_Template_Parser::build_schema_from_field_definitions( $fields );
if ( ! is_array( $raw_schema ) ) {
return array();
}

$schema = array();
foreach ( $raw_schema as $entry ) {
if ( ! is_array( $entry ) || empty( $entry['slug'] ) ) {
continue;
}

$slug = sanitize_key( $entry['slug'] );
$label = isset( $entry['label'] ) ? sanitize_text_field( $entry['label'] ) : $this->humanize_slug( $slug );
$type = isset( $entry['type'] ) ? sanitize_key( $entry['type'] ) : 'textarea';
$placeholder = isset( $entry['placeholder'] ) ? $this->sanitize_placeholder_name( $entry['placeholder'] ) : $slug;
$data_type = isset( $entry['data_type'] ) ? sanitize_key( $entry['data_type'] ) : 'text';

if ( '' === $slug ) {
continue;
}
if ( '' === $label ) {
$label = $this->humanize_slug( $slug );
}
if ( '' === $placeholder ) {
$placeholder = $slug;
}

if ( 'array' === $type ) {
$item_schema = array();
if ( isset( $entry['item_schema'] ) && is_array( $entry['item_schema'] ) ) {
foreach ( $entry['item_schema'] as $key => $item ) {
$item_key = sanitize_key( $key );
if ( '' === $item_key ) {
continue;
}
$item_label = isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : $this->humanize_slug( $item_key );
$item_type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : 'textarea';
if ( ! in_array( $item_type, array( 'single', 'textarea', 'rich' ), true ) ) {
$item_type = 'textarea';
}
$item_data_type = isset( $item['data_type'] ) ? sanitize_key( $item['data_type'] ) : 'text';
if ( ! in_array( $item_data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$item_data_type = 'text';
}
$item_schema[ $item_key ] = array(
'label' => $item_label,
'type' => $item_type,
'data_type' => $item_data_type,
);
}
}

$schema[] = array(
'slug' => $slug,
'label' => $label,
'type' => 'array',
'placeholder' => $placeholder,
'data_type' => 'array',
'item_schema' => $item_schema,
);
continue;
}

if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}
if ( ! in_array( $data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$data_type = 'text';
}

$schema[] = array(
'slug' => $slug,
'label' => $label,
'type' => $type,
'placeholder' => $placeholder,
'data_type' => $data_type,
);
}

return $schema;
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method build_schema_from_fields() has a Cyclomatic Complexity of 24. The configured cyclomatic complexity threshold is 10.
Comment on lines +291 to +372
private function build_schema_from_fields( $fields ) {
$raw_schema = Resolate_Template_Parser::build_schema_from_field_definitions( $fields );
if ( ! is_array( $raw_schema ) ) {
return array();
}

$schema = array();
foreach ( $raw_schema as $entry ) {
if ( ! is_array( $entry ) || empty( $entry['slug'] ) ) {
continue;
}

$slug = sanitize_key( $entry['slug'] );
$label = isset( $entry['label'] ) ? sanitize_text_field( $entry['label'] ) : $this->humanize_slug( $slug );
$type = isset( $entry['type'] ) ? sanitize_key( $entry['type'] ) : 'textarea';
$placeholder = isset( $entry['placeholder'] ) ? $this->sanitize_placeholder_name( $entry['placeholder'] ) : $slug;
$data_type = isset( $entry['data_type'] ) ? sanitize_key( $entry['data_type'] ) : 'text';

if ( '' === $slug ) {
continue;
}
if ( '' === $label ) {
$label = $this->humanize_slug( $slug );
}
if ( '' === $placeholder ) {
$placeholder = $slug;
}

if ( 'array' === $type ) {
$item_schema = array();
if ( isset( $entry['item_schema'] ) && is_array( $entry['item_schema'] ) ) {
foreach ( $entry['item_schema'] as $key => $item ) {
$item_key = sanitize_key( $key );
if ( '' === $item_key ) {
continue;
}
$item_label = isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : $this->humanize_slug( $item_key );
$item_type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : 'textarea';
if ( ! in_array( $item_type, array( 'single', 'textarea', 'rich' ), true ) ) {
$item_type = 'textarea';
}
$item_data_type = isset( $item['data_type'] ) ? sanitize_key( $item['data_type'] ) : 'text';
if ( ! in_array( $item_data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$item_data_type = 'text';
}
$item_schema[ $item_key ] = array(
'label' => $item_label,
'type' => $item_type,
'data_type' => $item_data_type,
);
}
}

$schema[] = array(
'slug' => $slug,
'label' => $label,
'type' => 'array',
'placeholder' => $placeholder,
'data_type' => 'array',
'item_schema' => $item_schema,
);
continue;
}

if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}
if ( ! in_array( $data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$data_type = 'text';
}

$schema[] = array(
'slug' => $slug,
'label' => $label,
'type' => $type,
'placeholder' => $placeholder,
'data_type' => $data_type,
);
}

return $schema;
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method build_schema_from_fields() has an NPath complexity of 208898. The configured NPath complexity threshold is 200.
Comment on lines 436 to +481
@@ -404,81 +453,81 @@ private static function build_output_path( $post_id, $extension ) {
*
* @return string Absolute directory path.
*/
private static function ensure_output_dir() {
$upload_dir = wp_upload_dir();
$dir = trailingslashit( $upload_dir['basedir'] ) . 'resolate';
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
}

return $dir;
}

/**
* Sanitize placeholders preserving TinyButStrong supported characters.
*
* @param string $placeholder Placeholder name.
* @return string
*/
private static function sanitize_placeholder_name( $placeholder ) {
$placeholder = (string) $placeholder;
$placeholder = preg_replace( '/[^A-Za-z0-9._:-]/', '', $placeholder );
return $placeholder;
}

/**
* Normalize a field value based on the detected data type.
*
* @param string $value Original value.
* @param string $data_type Detected data type.
* @return mixed
*/
private static function normalize_field_value( $value, $data_type ) {
$value = is_string( $value ) ? trim( $value ) : $value;
$data_type = sanitize_key( $data_type );

switch ( $data_type ) {
case 'number':
if ( '' === $value ) {
return '';
}
if ( is_numeric( $value ) ) {
return 0 + $value;
}
$filtered = preg_replace( '/[^0-9.,\-]/', '', (string) $value );
if ( '' === $filtered ) {
return '';
}
$normalized = str_replace( ',', '.', $filtered );
if ( is_numeric( $normalized ) ) {
return 0 + $normalized;
}
return $value;
case 'boolean':
if ( is_bool( $value ) ) {
return $value ? 1 : 0;
}
$value = strtolower( (string) $value );
if ( in_array( $value, array( '1', 'true', 'si', 'sí', 'yes', 'on' ), true ) ) {
return 1;
}
if ( in_array( $value, array( '0', 'false', 'no', 'off' ), true ) ) {
return 0;
}
return '' === $value ? 0 : 0;
case 'date':
if ( '' === $value ) {
return '';
}
$timestamp = strtotime( (string) $value );
if ( false === $timestamp ) {
return $value;
}
return wp_date( 'Y-m-d', $timestamp );
default:
return $value;
}
}
private static function ensure_output_dir() {
$upload_dir = wp_upload_dir();
$dir = trailingslashit( $upload_dir['basedir'] ) . 'resolate';
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
}

return $dir;
}

/**
* Sanitize placeholders preserving TinyButStrong supported characters.
*
* @param string $placeholder Placeholder name.
* @return string
*/
private static function sanitize_placeholder_name( $placeholder ) {
$placeholder = (string) $placeholder;
$placeholder = preg_replace( '/[^A-Za-z0-9._:-]/', '', $placeholder );
return $placeholder;
}

/**
* Normalize a field value based on the detected data type.
*
* @param string $value Original value.
* @param string $data_type Detected data type.
* @return mixed
*/
private static function normalize_field_value( $value, $data_type ) {
$value = is_string( $value ) ? trim( $value ) : $value;
$data_type = sanitize_key( $data_type );

switch ( $data_type ) {
case 'number':
if ( '' === $value ) {
return '';
}
if ( is_numeric( $value ) ) {
return 0 + $value;
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method normalize_field_value() has a Cyclomatic Complexity of 16. The configured cyclomatic complexity threshold is 10.
Comment on lines +148 to +352
public static function build_schema_from_field_definitions( $fields ) {
if ( ! is_array( $fields ) ) {
return array();
}

$array_defs = array();
$array_order = array();
$repeat_hints = array();
$pending = array();
$order_counter = 0;

foreach ( $fields as $index => $field ) {
if ( ! is_array( $field ) ) {
continue;
}

$placeholder = isset( $field['placeholder'] ) ? trim( (string) $field['placeholder'] ) : '';
$parameters = isset( $field['parameters'] ) && is_array( $field['parameters'] ) ? $field['parameters'] : array();
$label = isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '';
$data_type = isset( $field['data_type'] ) ? sanitize_key( $field['data_type'] ) : 'text';

$array_match = self::detect_array_placeholder_with_index( $placeholder );
if ( $array_match ) {
$base = $array_match['base'];
$key = $array_match['key'];

if ( '' === $base || '' === $key ) {
continue;
}

$repeat_hints[ $base ] = true;

if ( ! isset( $array_defs[ $base ] ) ) {
$array_defs[ $base ] = array(
'slug' => $base,
'label' => self::humanize_key( $base ),
'type' => 'array',
'placeholder' => $base,
'data_type' => 'array',
'item_schema' => array(),
'_order' => $order_counter++,
);
$array_order[] = $base;
}

if ( '' === $label ) {
$label = self::humanize_key( $array_match['raw_key'] );
}

if ( ! isset( $array_defs[ $base ]['item_schema'][ $key ] ) ) {
$item_data_type = self::detect_data_type( $placeholder, $parameters );
if ( '' === $item_data_type ) {
$item_data_type = 'text';
}

$array_defs[ $base ]['item_schema'][ $key ] = array(
'label' => $label,
'type' => self::infer_array_item_type( $key, $item_data_type ),
'data_type' => $item_data_type,
);
}

continue;
}

if ( isset( $parameters['repeat'] ) ) {
$repeat_base = sanitize_key( $parameters['repeat'] );
if ( '' !== $repeat_base ) {
$repeat_hints[ $repeat_base ] = true;
}
}

$pending[] = array(
'field' => $field,
'placeholder' => $placeholder,
'parameters' => $parameters,
'label' => $label,
'data_type' => $data_type,
'index' => $index,
);
}

$schema = array();
$scalar_fields = array();

foreach ( $pending as $entry ) {
$placeholder = $entry['placeholder'];
$parameters = $entry['parameters'];
$label = $entry['label'];
$data_type = $entry['data_type'];

$dot_match = self::detect_array_placeholder_without_index( $placeholder );
if ( $dot_match && ( isset( $repeat_hints[ $dot_match['base'] ] ) || isset( $array_defs[ $dot_match['base'] ] ) ) ) {
$base = $dot_match['base'];
$key = $dot_match['key'];

if ( '' === $base || '' === $key ) {
continue;
}

if ( ! isset( $array_defs[ $base ] ) ) {
$array_defs[ $base ] = array(
'slug' => $base,
'label' => self::humanize_key( $base ),
'type' => 'array',
'placeholder' => $base,
'data_type' => 'array',
'item_schema' => array(),
'_order' => $order_counter++,
);
$array_order[] = $base;
}

if ( ! isset( $array_defs[ $base ]['item_schema'][ $key ] ) ) {
$item_data_type = self::detect_data_type( $placeholder, $parameters );
if ( '' === $label ) {
$label = self::humanize_key( $dot_match['raw_key'] );
}
if ( '' === $item_data_type ) {
$item_data_type = 'text';
}
$array_defs[ $base ]['item_schema'][ $key ] = array(
'label' => ( '' !== $label ) ? $label : self::humanize_key( $dot_match['raw_key'] ),
'type' => self::infer_array_item_type( $key, $item_data_type ),
'data_type' => $item_data_type,
);
}

continue;
}

$slug = isset( $entry['field']['slug'] ) ? sanitize_key( $entry['field']['slug'] ) : '';
if ( '' === $slug ) {
$slug = sanitize_key( $placeholder );
}

if ( '' === $slug ) {
continue;
}

$normalized_placeholder = '';
if ( '' !== $placeholder ) {
$normalized_placeholder = preg_replace( '/[^A-Za-z0-9._:-]/', '', $placeholder );
}
if ( '' === $label ) {
$source = '' !== $normalized_placeholder ? $normalized_placeholder : $slug;
$label = self::humanize_key( $source );
}

if ( '' === $normalized_placeholder ) {
$normalized_placeholder = $slug;
}

if ( ! in_array( $data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$data_type = 'text';
}

if ( in_array( $slug, array( 'onshow', 'ondata', 'block', 'var' ), true ) ) {
continue;
}

$scalar_fields[] = array(
'slug' => $slug,
'label' => $label,
'placeholder' => $normalized_placeholder,
'data_type' => $data_type,
'_order' => $entry['index'],
);
}

if ( ! empty( $array_defs ) ) {
uasort(
$array_defs,
static function ( $a, $b ) {
$order_a = isset( $a['_order'] ) ? intval( $a['_order'] ) : 0;
$order_b = isset( $b['_order'] ) ? intval( $b['_order'] ) : 0;
return $order_a <=> $order_b;
}
);

foreach ( $array_defs as $base => $def ) {
unset( $def['_order'] );
$schema[] = $def;
}
}

if ( ! empty( $scalar_fields ) ) {
usort(
$scalar_fields,
static function ( $a, $b ) {
$order_a = isset( $a['_order'] ) ? intval( $a['_order'] ) : 0;
$order_b = isset( $b['_order'] ) ? intval( $b['_order'] ) : 0;
return $order_a <=> $order_b;
}
);

foreach ( $scalar_fields as $field ) {
unset( $field['_order'] );
$field['type'] = 'textarea';
$schema[] = $field;
}
}

return $schema;
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method build_schema_from_field_definitions() has a Cyclomatic Complexity of 50. The configured cyclomatic complexity threshold is 10.
Comment on lines +148 to +352
public static function build_schema_from_field_definitions( $fields ) {
if ( ! is_array( $fields ) ) {
return array();
}

$array_defs = array();
$array_order = array();
$repeat_hints = array();
$pending = array();
$order_counter = 0;

foreach ( $fields as $index => $field ) {
if ( ! is_array( $field ) ) {
continue;
}

$placeholder = isset( $field['placeholder'] ) ? trim( (string) $field['placeholder'] ) : '';
$parameters = isset( $field['parameters'] ) && is_array( $field['parameters'] ) ? $field['parameters'] : array();
$label = isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '';
$data_type = isset( $field['data_type'] ) ? sanitize_key( $field['data_type'] ) : 'text';

$array_match = self::detect_array_placeholder_with_index( $placeholder );
if ( $array_match ) {
$base = $array_match['base'];
$key = $array_match['key'];

if ( '' === $base || '' === $key ) {
continue;
}

$repeat_hints[ $base ] = true;

if ( ! isset( $array_defs[ $base ] ) ) {
$array_defs[ $base ] = array(
'slug' => $base,
'label' => self::humanize_key( $base ),
'type' => 'array',
'placeholder' => $base,
'data_type' => 'array',
'item_schema' => array(),
'_order' => $order_counter++,
);
$array_order[] = $base;
}

if ( '' === $label ) {
$label = self::humanize_key( $array_match['raw_key'] );
}

if ( ! isset( $array_defs[ $base ]['item_schema'][ $key ] ) ) {
$item_data_type = self::detect_data_type( $placeholder, $parameters );
if ( '' === $item_data_type ) {
$item_data_type = 'text';
}

$array_defs[ $base ]['item_schema'][ $key ] = array(
'label' => $label,
'type' => self::infer_array_item_type( $key, $item_data_type ),
'data_type' => $item_data_type,
);
}

continue;
}

if ( isset( $parameters['repeat'] ) ) {
$repeat_base = sanitize_key( $parameters['repeat'] );
if ( '' !== $repeat_base ) {
$repeat_hints[ $repeat_base ] = true;
}
}

$pending[] = array(
'field' => $field,
'placeholder' => $placeholder,
'parameters' => $parameters,
'label' => $label,
'data_type' => $data_type,
'index' => $index,
);
}

$schema = array();
$scalar_fields = array();

foreach ( $pending as $entry ) {
$placeholder = $entry['placeholder'];
$parameters = $entry['parameters'];
$label = $entry['label'];
$data_type = $entry['data_type'];

$dot_match = self::detect_array_placeholder_without_index( $placeholder );
if ( $dot_match && ( isset( $repeat_hints[ $dot_match['base'] ] ) || isset( $array_defs[ $dot_match['base'] ] ) ) ) {
$base = $dot_match['base'];
$key = $dot_match['key'];

if ( '' === $base || '' === $key ) {
continue;
}

if ( ! isset( $array_defs[ $base ] ) ) {
$array_defs[ $base ] = array(
'slug' => $base,
'label' => self::humanize_key( $base ),
'type' => 'array',
'placeholder' => $base,
'data_type' => 'array',
'item_schema' => array(),
'_order' => $order_counter++,
);
$array_order[] = $base;
}

if ( ! isset( $array_defs[ $base ]['item_schema'][ $key ] ) ) {
$item_data_type = self::detect_data_type( $placeholder, $parameters );
if ( '' === $label ) {
$label = self::humanize_key( $dot_match['raw_key'] );
}
if ( '' === $item_data_type ) {
$item_data_type = 'text';
}
$array_defs[ $base ]['item_schema'][ $key ] = array(
'label' => ( '' !== $label ) ? $label : self::humanize_key( $dot_match['raw_key'] ),
'type' => self::infer_array_item_type( $key, $item_data_type ),
'data_type' => $item_data_type,
);
}

continue;
}

$slug = isset( $entry['field']['slug'] ) ? sanitize_key( $entry['field']['slug'] ) : '';
if ( '' === $slug ) {
$slug = sanitize_key( $placeholder );
}

if ( '' === $slug ) {
continue;
}

$normalized_placeholder = '';
if ( '' !== $placeholder ) {
$normalized_placeholder = preg_replace( '/[^A-Za-z0-9._:-]/', '', $placeholder );
}
if ( '' === $label ) {
$source = '' !== $normalized_placeholder ? $normalized_placeholder : $slug;
$label = self::humanize_key( $source );
}

if ( '' === $normalized_placeholder ) {
$normalized_placeholder = $slug;
}

if ( ! in_array( $data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$data_type = 'text';
}

if ( in_array( $slug, array( 'onshow', 'ondata', 'block', 'var' ), true ) ) {
continue;
}

$scalar_fields[] = array(
'slug' => $slug,
'label' => $label,
'placeholder' => $normalized_placeholder,
'data_type' => $data_type,
'_order' => $entry['index'],
);
}

if ( ! empty( $array_defs ) ) {
uasort(
$array_defs,
static function ( $a, $b ) {
$order_a = isset( $a['_order'] ) ? intval( $a['_order'] ) : 0;
$order_b = isset( $b['_order'] ) ? intval( $b['_order'] ) : 0;
return $order_a <=> $order_b;
}
);

foreach ( $array_defs as $base => $def ) {
unset( $def['_order'] );
$schema[] = $def;
}
}

if ( ! empty( $scalar_fields ) ) {
usort(
$scalar_fields,
static function ( $a, $b ) {
$order_a = isset( $a['_order'] ) ? intval( $a['_order'] ) : 0;
$order_b = isset( $b['_order'] ) ? intval( $b['_order'] ) : 0;
return $order_a <=> $order_b;
}
);

foreach ( $scalar_fields as $field ) {
unset( $field['_order'] );
$field['type'] = 'textarea';
$schema[] = $field;
}
}

return $schema;
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method build_schema_from_field_definitions() has an NPath complexity of 96592348314. The configured NPath complexity threshold is 200.
Comment on lines +529 to +567
private function normalize_array_item_schema( $definition ) {
$schema = array();

if ( isset( $definition['item_schema'] ) && is_array( $definition['item_schema'] ) ) {
foreach ( $definition['item_schema'] as $key => $item ) {
$item_key = sanitize_key( $key );
if ( '' === $item_key ) {
continue;
}

$label = isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : $this->humanize_unknown_field_label( $item_key );
$type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : 'textarea';
if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}

$data_type = isset( $item['data_type'] ) ? sanitize_key( $item['data_type'] ) : 'text';
if ( ! in_array( $data_type, array( 'text', 'number', 'boolean', 'date' ), true ) ) {
$data_type = 'text';
}

$schema[ $item_key ] = array(
'label' => $label,
'type' => $type,
'data_type' => $data_type,
);
}
}

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
if ( empty( $schema ) ) {
$schema['content'] = array(
'label' => __( 'Contenido', 'resolate' ),
'type' => 'textarea',
'data_type' => 'text',
);
}

if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
return $schema;
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method normalize_array_item_schema() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10.
Comment on lines +561 to +612
'type' => 'textarea',
'data_type' => 'text',
);
}

if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
return $schema;
}

/**
* Render an array field with repeatable items.
*
* @param string $slug Field slug.
* @param string $label Field label.
* @param array $item_schema Item schema definition.
* @param array $items Current values.
* @return void
*/
private function render_array_field( $slug, $label, $item_schema, $items ) {
$slug = sanitize_key( $slug );
$label = sanitize_text_field( $label );
$field_id = 'resolate-array-' . $slug;
$items = is_array( $items ) ? $items : array();
$item_schema = is_array( $item_schema ) ? $item_schema : array();

echo '<div class="resolate-array-field" data-array-field="' . esc_attr( $slug ) . '" style="margin-bottom:24px;">';
echo '<div class="resolate-array-heading" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px;">';
echo '<span class="resolate-array-title" style="font-weight:600;font-size:15px;">' . esc_html( $label ) . '</span>';
echo '<button type="button" class="button button-secondary resolate-array-add" data-array-target="' . esc_attr( $slug ) . '">' . esc_html__( 'Añadir elemento', 'resolate' ) . '</button>';
echo '</div>';

echo '<div class="resolate-array-items" id="' . esc_attr( $field_id ) . '" data-field="' . esc_attr( $slug ) . '">';
foreach ( $items as $index => $values ) {
$values = is_array( $values ) ? $values : array();
$this->render_array_field_item( $slug, (string) $index, $item_schema, $values );
}
echo '</div>';

// Handle type selection (lock after set).
if ( isset( $_POST['resolate_type_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['resolate_type_nonce'] ) ), 'resolate_type_nonce' ) ) {
$posted = isset( $_POST['resolate_doc_type'] ) ? intval( $_POST['resolate_doc_type'] ) : 0;
$assigned = wp_get_post_terms( $post_id, 'resolate_doc_type', array( 'fields' => 'ids' ) );
$current = ( ! is_wp_error( $assigned ) && ! empty( $assigned ) ) ? intval( $assigned[0] ) : 0;
if ( $current <= 0 && $posted > 0 ) {
wp_set_post_terms( $post_id, array( $posted ), 'resolate_doc_type', false );
echo '<template class="resolate-array-template" data-field="' . esc_attr( $slug ) . '">';
$this->render_array_field_item( $slug, '__INDEX__', $item_schema, array(), true );
echo '</template>';
echo '</div>';
}

/**
* Render a single repeatable array item row.
*
* @param string $slug Field slug.
* @param string $index Item index.
* @param array $item_schema Item schema definition.
* @param array $values Current values.
* @param bool $is_template Whether the row is a template placeholder.
* @return void

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method save_meta_boxes() has a Cyclomatic Complexity of 13. The configured cyclomatic complexity threshold is 10.
Comment on lines +561 to +612
'type' => 'textarea',
'data_type' => 'text',
);
}

if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
return $schema;
}

/**
* Render an array field with repeatable items.
*
* @param string $slug Field slug.
* @param string $label Field label.
* @param array $item_schema Item schema definition.
* @param array $items Current values.
* @return void
*/
private function render_array_field( $slug, $label, $item_schema, $items ) {
$slug = sanitize_key( $slug );
$label = sanitize_text_field( $label );
$field_id = 'resolate-array-' . $slug;
$items = is_array( $items ) ? $items : array();
$item_schema = is_array( $item_schema ) ? $item_schema : array();

echo '<div class="resolate-array-field" data-array-field="' . esc_attr( $slug ) . '" style="margin-bottom:24px;">';
echo '<div class="resolate-array-heading" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px;">';
echo '<span class="resolate-array-title" style="font-weight:600;font-size:15px;">' . esc_html( $label ) . '</span>';
echo '<button type="button" class="button button-secondary resolate-array-add" data-array-target="' . esc_attr( $slug ) . '">' . esc_html__( 'Añadir elemento', 'resolate' ) . '</button>';
echo '</div>';

echo '<div class="resolate-array-items" id="' . esc_attr( $field_id ) . '" data-field="' . esc_attr( $slug ) . '">';
foreach ( $items as $index => $values ) {
$values = is_array( $values ) ? $values : array();
$this->render_array_field_item( $slug, (string) $index, $item_schema, $values );
}
echo '</div>';

// Handle type selection (lock after set).
if ( isset( $_POST['resolate_type_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['resolate_type_nonce'] ) ), 'resolate_type_nonce' ) ) {
$posted = isset( $_POST['resolate_doc_type'] ) ? intval( $_POST['resolate_doc_type'] ) : 0;
$assigned = wp_get_post_terms( $post_id, 'resolate_doc_type', array( 'fields' => 'ids' ) );
$current = ( ! is_wp_error( $assigned ) && ! empty( $assigned ) ) ? intval( $assigned[0] ) : 0;
if ( $current <= 0 && $posted > 0 ) {
wp_set_post_terms( $post_id, array( $posted ), 'resolate_doc_type', false );
echo '<template class="resolate-array-template" data-field="' . esc_attr( $slug ) . '">';
$this->render_array_field_item( $slug, '__INDEX__', $item_schema, array(), true );
echo '</template>';
echo '</div>';
}

/**
* Render a single repeatable array item row.
*
* @param string $slug Field slug.
* @param string $index Item index.
* @param array $item_schema Item schema definition.
* @param array $values Current values.
* @param bool $is_template Whether the row is a template placeholder.
* @return void

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method save_meta_boxes() has an NPath complexity of 252. The configured NPath complexity threshold is 200.
Comment on lines +614 to +656
private function render_array_field_item( $slug, $index, $item_schema, $values, $is_template = false ) {
$slug = sanitize_key( $slug );
$index_attr = (string) $index;
$item_schema = is_array( $item_schema ) ? $item_schema : array();
$values = is_array( $values ) ? $values : array();

echo '<div class="resolate-array-item" data-index="' . esc_attr( $index_attr ) . '" draggable="true" style="border:1px solid #e5e5e5;padding:16px;margin-bottom:12px;background:#fff;">';
echo '<div class="resolate-array-item-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;">';
echo '<span class="resolate-array-handle" role="button" tabindex="0" aria-label="' . esc_attr__( 'Mover elemento', 'resolate' ) . '" style="cursor:move;user-select:none;">≡</span>';
echo '<button type="button" class="button-link-delete resolate-array-remove">' . esc_html__( 'Eliminar', 'resolate' ) . '</button>';
echo '</div>';

foreach ( $item_schema as $key => $definition ) {
$item_key = sanitize_key( $key );
if ( '' === $item_key ) {
continue;
}

$field_name = 'tpl_fields[' . $slug . '][' . $index_attr . '][' . $item_key . ']';
$field_id = 'resolate-' . $slug . '-' . $item_key . '-' . $index_attr;
$label = isset( $definition['label'] ) ? sanitize_text_field( $definition['label'] ) : $this->humanize_unknown_field_label( $item_key );
$type = isset( $definition['type'] ) ? sanitize_key( $definition['type'] ) : 'textarea';
$value = isset( $values[ $item_key ] ) ? (string) $values[ $item_key ] : '';

if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}

echo '<div class="resolate-array-field-control" style="margin-bottom:12px;">';
echo '<label for="' . esc_attr( $field_id ) . '" style="font-weight:600;display:block;margin-bottom:4px;">' . esc_html( $label ) . '</label>';

if ( 'single' === $type ) {
echo '<input type="text" class="widefat" id="' . esc_attr( $field_id ) . '" name="' . esc_attr( $field_name ) . '" value="' . esc_attr( $value ) . '" />';
} else {
$rows = ( 'rich' === $type ) ? 8 : 4;
echo '<textarea class="widefat" rows="' . esc_attr( (string) $rows ) . '" id="' . esc_attr( $field_id ) . '" name="' . esc_attr( $field_name ) . '">' . esc_textarea( $value ) . '</textarea>';
}

echo '</div>';
}

echo '</div>';
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method render_array_field_item() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10.
Comment on lines +614 to +656
private function render_array_field_item( $slug, $index, $item_schema, $values, $is_template = false ) {
$slug = sanitize_key( $slug );
$index_attr = (string) $index;
$item_schema = is_array( $item_schema ) ? $item_schema : array();
$values = is_array( $values ) ? $values : array();

echo '<div class="resolate-array-item" data-index="' . esc_attr( $index_attr ) . '" draggable="true" style="border:1px solid #e5e5e5;padding:16px;margin-bottom:12px;background:#fff;">';
echo '<div class="resolate-array-item-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;">';
echo '<span class="resolate-array-handle" role="button" tabindex="0" aria-label="' . esc_attr__( 'Mover elemento', 'resolate' ) . '" style="cursor:move;user-select:none;">≡</span>';
echo '<button type="button" class="button-link-delete resolate-array-remove">' . esc_html__( 'Eliminar', 'resolate' ) . '</button>';
echo '</div>';

foreach ( $item_schema as $key => $definition ) {
$item_key = sanitize_key( $key );
if ( '' === $item_key ) {
continue;
}

$field_name = 'tpl_fields[' . $slug . '][' . $index_attr . '][' . $item_key . ']';
$field_id = 'resolate-' . $slug . '-' . $item_key . '-' . $index_attr;
$label = isset( $definition['label'] ) ? sanitize_text_field( $definition['label'] ) : $this->humanize_unknown_field_label( $item_key );
$type = isset( $definition['type'] ) ? sanitize_key( $definition['type'] ) : 'textarea';
$value = isset( $values[ $item_key ] ) ? (string) $values[ $item_key ] : '';

if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}

echo '<div class="resolate-array-field-control" style="margin-bottom:12px;">';
echo '<label for="' . esc_attr( $field_id ) . '" style="font-weight:600;display:block;margin-bottom:4px;">' . esc_html( $label ) . '</label>';

if ( 'single' === $type ) {
echo '<input type="text" class="widefat" id="' . esc_attr( $field_id ) . '" name="' . esc_attr( $field_name ) . '" value="' . esc_attr( $value ) . '" />';
} else {
$rows = ( 'rich' === $type ) ? 8 : 4;
echo '<textarea class="widefat" rows="' . esc_attr( (string) $rows ) . '" id="' . esc_attr( $field_id ) . '" name="' . esc_attr( $field_name ) . '">' . esc_textarea( $value ) . '</textarea>';
}

echo '</div>';
}

echo '</div>';
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method render_array_field_item() has an NPath complexity of 388. The configured NPath complexity threshold is 200.
@erseco erseco merged commit 113da93 into main Oct 14, 2025
3 of 4 checks passed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

Comment on lines 1154 to +1173
if ( ! is_array( $item ) ) {
continue;
}
$slug = isset( $item['slug'] ) ? sanitize_key( $item['slug'] ) : '';
$label = isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : '';
$type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : 'textarea';
$placeholder = isset( $item['placeholder'] ) ? preg_replace( '/[^A-Za-z0-9._:-]/', '', (string) $item['placeholder'] ) : '';
$data_type = isset( $item['data_type'] ) ? sanitize_key( $item['data_type'] ) : '';
if ( '' === $slug || '' === $label ) {
continue;
}
if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}
$out[] = array(
'slug' => $slug,
'label' => $label,
'type' => $type,
'placeholder' => $placeholder,
'data_type' => $data_type,
);
}
return $out;
}
$slug = isset( $item['slug'] ) ? sanitize_key( $item['slug'] ) : '';
$label = isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : '';
$type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : 'textarea';
$placeholder = isset( $item['placeholder'] ) ? preg_replace( '/[^A-Za-z0-9._:-]/', '', (string) $item['placeholder'] ) : '';
$data_type = isset( $item['data_type'] ) ? sanitize_key( $item['data_type'] ) : '';
if ( '' === $slug || '' === $label ) {
continue;
}
if ( ! in_array( $type, array( 'single', 'textarea', 'rich' ), true ) ) {
$type = 'textarea';
}
$out[] = array(
'slug' => $slug,
'label' => $label,
'type' => $type,
'placeholder' => $placeholder,
'data_type' => $data_type,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve array field type when loading schema

The new repeatable array feature relies on schema entries whose type is 'array' so filter_post_data_compose_content() and Resolate_Document_Generator::build_merge_fields() can JSON‑encode posted items and decode them for template merges. However, get_term_schema() still normalizes every field type to one of single|textarea|rich and forcibly rewrites anything else to 'textarea'. When a template defines an array field and the schema is saved, this method returns it as a textarea, so the later code never enters the array branch and ignores values submitted in tpl_fields[...]. As a result array data is never persisted or exported unless the schema is manually patched. Include 'array' in the allowed types and keep the original item_schema so array fields can be recognized downstream.

Useful? React with 👍 / 👎.

@erseco erseco deleted the feature/add-array-fields-for-annexes-support-cmdtk9 branch November 30, 2025 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants