Skip to content

Conversation

@erseco
Copy link
Collaborator

@erseco erseco commented Jan 30, 2026

This pull request introduces a comprehensive "Archive" workflow for Documentate documents, allowing administrators to archive published documents and unarchive them as needed. It adds a new custom post status (archived), enforces strict rules for transitioning to and from this status, and updates both the admin UI and workflow logic to support these changes. Additionally, it improves styling and messaging for archived documents throughout the plugin.

Workflow & Permissions

  • Added a new custom post status archived for documents, with registration, admin visibility, and label configuration. Only administrators can archive published documents or unarchive archived ones. Non-admins are prevented from archiving or modifying archived documents, with appropriate error messages surfaced. (includes/class-documentate-workflow.php) [1] [2] [3] [4]
  • Updated admin row actions to include "Archive" and "Unarchive" options for administrators, with nonce validation and status transitions handled via new endpoints. (includes/class-documentate-admin-helper.php) [1] [2]

Admin UI & Styling

  • Added new CSS classes and styles for archived documents and workflow notices in the admin interface, including list table row styling and metabox visuals. (admin/css/documentate-workflow.css) [1] [2] [3]
  • Updated workflow metabox and status icons to include "Archived" status and appropriate dashicons. (includes/class-documentate-workflow.php)

JavaScript Workflow Logic

  • Updated JS logic to treat archived documents as locked, similar to published ones, showing appropriate icons and messages for archived state, and ensuring editors are disabled for both published and archived documents. (admin/js/documentate-workflow.js) [1] [2] [3] [4] [5]
  • Localized new strings for archived state and added flags for isArchived and improved locking logic. (includes/class-documentate-workflow.php) [1] [2]

Miscellaneous

  • Updated Makefile to use the correct CLI environment for language installation and test running, and removed redundant testdox output. (Makefile) [1] [2] [3]
  • Improved title extraction in document generation to use raw post title for better accuracy. (includes/class-documentate-document-generator.php)
  • Added general development rules to CLAUDE.md to emphasize clean, long-term solutions and avoid workarounds. (CLAUDE.md)

Comment on lines +50 to +122
public static function import_fixture_file( $filename ) {
$base_dir = self::$plugin_dir;
$paths = array(
$base_dir . 'fixtures/' . $filename,
$base_dir . $filename,
);
$source = '';
foreach ( $paths as $p ) {
if ( file_exists( $p ) && is_readable( $p ) ) {
$source = $p;
break;
}
}
if ( '' === $source ) {
return 0;
}

$hash = @md5_file( $source );
if ( $hash ) {
$found = get_posts(
array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'meta_key' => '_documentate_fixture_hash',
'meta_value' => $hash,
'fields' => 'ids',
'numberposts' => 1,
)
);
if ( ! empty( $found ) ) {
return intval( $found[0] );
}
}

$contents = @file_get_contents( $source );
if ( false === $contents ) {
return 0;
}

$upload = wp_upload_bits( basename( $source ), null, $contents );
if ( ! empty( $upload['error'] ) ) {
return 0;
}

$filetype = wp_check_filetype_and_ext( $upload['file'], basename( $upload['file'] ) );
$attachment = array(
'post_mime_type' => $filetype['type'] ? $filetype['type'] : 'application/octet-stream',
'post_title' => sanitize_file_name( basename( $source ) ),
'post_content' => '',
'post_status' => 'inherit',
);
$attach_id = wp_insert_attachment( $attachment, $upload['file'] );
if ( ! $attach_id ) {
return 0;
}

// Generate and save attachment metadata (for images).
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
if ( ! empty( $attach_data ) ) {
wp_update_attachment_metadata( $attach_id, $attach_data );
}

// Tag as fixture to allow reuse.
if ( $hash ) {
update_post_meta( $attach_id, '_documentate_fixture_hash', $hash );
}
update_post_meta( $attach_id, '_documentate_fixture_name', basename( $source ) );

return intval( $attach_id );
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method import_fixture_file() has an NPath complexity of 3072. The configured NPath complexity threshold is 500.
Comment on lines +147 to +233
public static function maybe_seed_default_doc_types() {
if ( ! taxonomy_exists( 'documentate_doc_type' ) ) {
return;
}

self::ensure_default_media();

$definitions = self::get_doc_type_definitions();

if ( empty( $definitions ) ) {
return;
}

foreach ( $definitions as $definition ) {
$slug = $definition['slug'];
$template_id = intval( $definition['template_id'] );
if ( $template_id <= 0 ) {
continue;
}

$term = get_term_by( 'slug', $slug, 'documentate_doc_type' );
$term_id = $term instanceof WP_Term ? intval( $term->term_id ) : 0;

if ( $term_id <= 0 ) {
$created = wp_insert_term(
$definition['name'],
'documentate_doc_type',
array(
'slug' => $slug,
'description' => $definition['description'],
)
);

if ( is_wp_error( $created ) ) {
continue;
}

$term_id = intval( $created['term_id'] );
}

if ( $term_id <= 0 ) {
continue;
}

$fixture_key = get_term_meta( $term_id, '_documentate_fixture', true );
if ( ! empty( $fixture_key ) && $fixture_key !== $definition['fixture_key'] ) {
continue;
}

update_term_meta( $term_id, '_documentate_fixture', $definition['fixture_key'] );
update_term_meta( $term_id, 'documentate_type_color', $definition['color'] );
update_term_meta( $term_id, 'documentate_type_template_id', $template_id );

$path = get_attached_file( $template_id );
if ( ! $path ) {
continue;
}

$extractor = new Documentate\DocType\SchemaExtractor();
$storage = new Documentate\DocType\SchemaStorage();

$existing_schema = $storage->get_schema( $term_id );
$template_hash = @md5_file( $path );

if ( ! empty( $existing_schema ) && $template_hash && isset( $existing_schema['meta']['hash'] ) && $template_hash === $existing_schema['meta']['hash'] ) {
$template_type = isset( $existing_schema['meta']['template_type'] ) ? (string) $existing_schema['meta']['template_type'] : strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) );
update_term_meta( $term_id, 'documentate_type_template_type', $template_type );
continue;
}

$schema = $extractor->extract( $path );
if ( is_wp_error( $schema ) ) {
continue;
}

$schema['meta']['template_id'] = $template_id;
$schema['meta']['template_type'] = isset( $schema['meta']['template_type'] ) ? (string) $schema['meta']['template_type'] : strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) );
$schema['meta']['template_name'] = basename( $path );
if ( empty( $schema['meta']['hash'] ) && $template_hash ) {
$schema['meta']['hash'] = $template_hash;
}

update_term_meta( $term_id, 'documentate_type_template_type', $schema['meta']['template_type'] );

$storage->save_schema( $term_id, $schema );
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method maybe_seed_default_doc_types() has a Cyclomatic Complexity of 21. The configured cyclomatic complexity threshold is 15.
Comment on lines +147 to +233
public static function maybe_seed_default_doc_types() {
if ( ! taxonomy_exists( 'documentate_doc_type' ) ) {
return;
}

self::ensure_default_media();

$definitions = self::get_doc_type_definitions();

if ( empty( $definitions ) ) {
return;
}

foreach ( $definitions as $definition ) {
$slug = $definition['slug'];
$template_id = intval( $definition['template_id'] );
if ( $template_id <= 0 ) {
continue;
}

$term = get_term_by( 'slug', $slug, 'documentate_doc_type' );
$term_id = $term instanceof WP_Term ? intval( $term->term_id ) : 0;

if ( $term_id <= 0 ) {
$created = wp_insert_term(
$definition['name'],
'documentate_doc_type',
array(
'slug' => $slug,
'description' => $definition['description'],
)
);

if ( is_wp_error( $created ) ) {
continue;
}

$term_id = intval( $created['term_id'] );
}

if ( $term_id <= 0 ) {
continue;
}

$fixture_key = get_term_meta( $term_id, '_documentate_fixture', true );
if ( ! empty( $fixture_key ) && $fixture_key !== $definition['fixture_key'] ) {
continue;
}

update_term_meta( $term_id, '_documentate_fixture', $definition['fixture_key'] );
update_term_meta( $term_id, 'documentate_type_color', $definition['color'] );
update_term_meta( $term_id, 'documentate_type_template_id', $template_id );

$path = get_attached_file( $template_id );
if ( ! $path ) {
continue;
}

$extractor = new Documentate\DocType\SchemaExtractor();
$storage = new Documentate\DocType\SchemaStorage();

$existing_schema = $storage->get_schema( $term_id );
$template_hash = @md5_file( $path );

if ( ! empty( $existing_schema ) && $template_hash && isset( $existing_schema['meta']['hash'] ) && $template_hash === $existing_schema['meta']['hash'] ) {
$template_type = isset( $existing_schema['meta']['template_type'] ) ? (string) $existing_schema['meta']['template_type'] : strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) );
update_term_meta( $term_id, 'documentate_type_template_type', $template_type );
continue;
}

$schema = $extractor->extract( $path );
if ( is_wp_error( $schema ) ) {
continue;
}

$schema['meta']['template_id'] = $template_id;
$schema['meta']['template_type'] = isset( $schema['meta']['template_type'] ) ? (string) $schema['meta']['template_type'] : strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) );
$schema['meta']['template_name'] = basename( $path );
if ( empty( $schema['meta']['hash'] ) && $template_hash ) {
$schema['meta']['hash'] = $template_hash;
}

update_term_meta( $term_id, 'documentate_type_template_type', $schema['meta']['template_type'] );

$storage->save_schema( $term_id, $schema );
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method maybe_seed_default_doc_types() has an NPath complexity of 41476. The configured NPath complexity threshold is 500.
Comment on lines +335 to +440
public static function maybe_seed_demo_documents() {
if ( ! post_type_exists( 'documentate_document' ) || ! taxonomy_exists( 'documentate_doc_type' ) ) {
return;
}

$should_seed = (bool) get_option( 'documentate_seed_demo_documents', false );
if ( ! $should_seed ) {
return;
}

// Check if demo documents already exist - if so, skip seeding.
$existing_demos = get_posts(
array(
'post_type' => 'documentate_document',
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_documentate_demo_key',
'compare' => 'EXISTS',
),
array(
'key' => '_documentate_demo_type_id',
'compare' => 'EXISTS',
),
),
)
);

if ( ! empty( $existing_demos ) ) {
delete_option( 'documentate_seed_demo_documents' );
return;
}

self::maybe_seed_default_doc_types();

// Get the Resolución Administrativa document type.
$term = get_term_by( 'slug', 'resolucion-administrativa', 'documentate_doc_type' );
if ( $term instanceof WP_Term ) {
// Create the 3 specific demo documents for Resolución Administrativa.
self::create_resolucion_demo_documents( $term );
}

// Create specific demo document for Autorización de viaje.
$autorizacion_term = get_term_by( 'slug', 'autorizacion-viaje', 'documentate_doc_type' );
if ( $autorizacion_term instanceof WP_Term ) {
self::create_specific_demo_documents( $autorizacion_term, self::get_autorizacion_viaje_demo() );
}

// Create specific demo document for Gastos suplidos.
$gastos_term = get_term_by( 'slug', 'gastos-suplidos', 'documentate_doc_type' );
if ( $gastos_term instanceof WP_Term ) {
self::create_specific_demo_documents( $gastos_term, self::get_gastos_suplidos_demo() );
}

// Create specific demo document for Propuesta de gasto.
$propuesta_term = get_term_by( 'slug', 'propuesta-gasto', 'documentate_doc_type' );
if ( $propuesta_term instanceof WP_Term ) {
self::create_specific_demo_documents( $propuesta_term, self::get_propuesta_gasto_demo() );
}

// Create specific demo document for Convocatoria de reunión.
$convocatoria_term = get_term_by( 'slug', 'convocatoria-reunion', 'documentate_doc_type' );
if ( $convocatoria_term instanceof WP_Term ) {
self::create_specific_demo_documents( $convocatoria_term, self::get_convocatoria_reunion_demo() );
}

// Also create demo documents for other document types (advanced demos).
$exclude_ids = array();
if ( $term instanceof WP_Term ) {
$exclude_ids[] = $term->term_id;
}
if ( $autorizacion_term instanceof WP_Term ) {
$exclude_ids[] = $autorizacion_term->term_id;
}
if ( $gastos_term instanceof WP_Term ) {
$exclude_ids[] = $gastos_term->term_id;
}
if ( $propuesta_term instanceof WP_Term ) {
$exclude_ids[] = $propuesta_term->term_id;
}
if ( $convocatoria_term instanceof WP_Term ) {
$exclude_ids[] = $convocatoria_term->term_id;
}

$terms = get_terms(
array(
'taxonomy' => 'documentate_doc_type',
'hide_empty' => false,
'exclude' => $exclude_ids,
)
);

if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
foreach ( $terms as $other_term ) {
if ( self::demo_document_exists( $other_term->term_id ) ) {
continue;
}
self::create_demo_document_for_type( $other_term );
}
}

delete_option( 'documentate_seed_demo_documents' );
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method maybe_seed_demo_documents() has a Cyclomatic Complexity of 19. The configured cyclomatic complexity threshold is 15.
Comment on lines +335 to +440
public static function maybe_seed_demo_documents() {
if ( ! post_type_exists( 'documentate_document' ) || ! taxonomy_exists( 'documentate_doc_type' ) ) {
return;
}

$should_seed = (bool) get_option( 'documentate_seed_demo_documents', false );
if ( ! $should_seed ) {
return;
}

// Check if demo documents already exist - if so, skip seeding.
$existing_demos = get_posts(
array(
'post_type' => 'documentate_document',
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => '_documentate_demo_key',
'compare' => 'EXISTS',
),
array(
'key' => '_documentate_demo_type_id',
'compare' => 'EXISTS',
),
),
)
);

if ( ! empty( $existing_demos ) ) {
delete_option( 'documentate_seed_demo_documents' );
return;
}

self::maybe_seed_default_doc_types();

// Get the Resolución Administrativa document type.
$term = get_term_by( 'slug', 'resolucion-administrativa', 'documentate_doc_type' );
if ( $term instanceof WP_Term ) {
// Create the 3 specific demo documents for Resolución Administrativa.
self::create_resolucion_demo_documents( $term );
}

// Create specific demo document for Autorización de viaje.
$autorizacion_term = get_term_by( 'slug', 'autorizacion-viaje', 'documentate_doc_type' );
if ( $autorizacion_term instanceof WP_Term ) {
self::create_specific_demo_documents( $autorizacion_term, self::get_autorizacion_viaje_demo() );
}

// Create specific demo document for Gastos suplidos.
$gastos_term = get_term_by( 'slug', 'gastos-suplidos', 'documentate_doc_type' );
if ( $gastos_term instanceof WP_Term ) {
self::create_specific_demo_documents( $gastos_term, self::get_gastos_suplidos_demo() );
}

// Create specific demo document for Propuesta de gasto.
$propuesta_term = get_term_by( 'slug', 'propuesta-gasto', 'documentate_doc_type' );
if ( $propuesta_term instanceof WP_Term ) {
self::create_specific_demo_documents( $propuesta_term, self::get_propuesta_gasto_demo() );
}

// Create specific demo document for Convocatoria de reunión.
$convocatoria_term = get_term_by( 'slug', 'convocatoria-reunion', 'documentate_doc_type' );
if ( $convocatoria_term instanceof WP_Term ) {
self::create_specific_demo_documents( $convocatoria_term, self::get_convocatoria_reunion_demo() );
}

// Also create demo documents for other document types (advanced demos).
$exclude_ids = array();
if ( $term instanceof WP_Term ) {
$exclude_ids[] = $term->term_id;
}
if ( $autorizacion_term instanceof WP_Term ) {
$exclude_ids[] = $autorizacion_term->term_id;
}
if ( $gastos_term instanceof WP_Term ) {
$exclude_ids[] = $gastos_term->term_id;
}
if ( $propuesta_term instanceof WP_Term ) {
$exclude_ids[] = $propuesta_term->term_id;
}
if ( $convocatoria_term instanceof WP_Term ) {
$exclude_ids[] = $convocatoria_term->term_id;
}

$terms = get_terms(
array(
'taxonomy' => 'documentate_doc_type',
'hide_empty' => false,
'exclude' => $exclude_ids,
)
);

if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
foreach ( $terms as $other_term ) {
if ( self::demo_document_exists( $other_term->term_id ) ) {
continue;
}
self::create_demo_document_for_type( $other_term );
}
}

delete_option( 'documentate_seed_demo_documents' );
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method maybe_seed_demo_documents() has an NPath complexity of 61440. The configured NPath complexity threshold is 500.
Comment on lines +1413 to +1571
public static function generate_demo_scalar_value( $slug, $type, $data_type, $index = 1, $context = array() ) {
$slug = strtolower( (string) $slug );
$type = sanitize_key( $type );
$data_type = sanitize_key( $data_type );
$index = max( 1, absint( $index ) );

$document_title = isset( $context['document_title'] ) ? (string) $context['document_title'] : __( 'Demo resolution', 'documentate' );
$number_value = (string) ( 1 + $index );

if ( 'date' === $data_type ) {
$month = max( 1, min( 12, $index ) );
$day = max( 1, min( 28, 10 + $index ) );
return sprintf( '2025-%02d-%02d', $month, $day );
}

if ( 'number' === $data_type ) {
return $number_value;
}

if ( 'boolean' === $data_type ) {
return ( $index % 2 ) ? '1' : '0';
}

if ( false !== strpos( $slug, 'email' ) ) {
return 'demo' . $index . '@ejemplo.es';
}

if ( false !== strpos( $slug, 'phone' ) || false !== strpos( $slug, 'tel' ) ) {
return '+3460000000' . $index;
}

if ( false !== strpos( $slug, 'dni' ) ) {
return '1234567' . $index . 'A';
}

if ( false !== strpos( $slug, 'url' ) || false !== strpos( $slug, 'sitio' ) || false !== strpos( $slug, 'web' ) ) {
return 'https://ejemplo.es/recurso-' . $index;
}

if ( false !== strpos( $slug, 'nombre' ) || false !== strpos( $slug, 'name' ) ) {
return ( 1 === $index ) ? 'Jane Doe' : 'John Smith';
}

if ( false !== strpos( $slug, 'title' ) || false !== strpos( $slug, 'titulo' ) || 'post_title' === $slug ) {
if ( 'post_title' === $slug ) {
return $document_title;
}

/* translators: %d: item sequence number. */
return sprintf( __( 'Demo item %d', 'documentate' ), $index );
}

if ( false !== strpos( $slug, 'summary' ) || false !== strpos( $slug, 'resumen' ) ) {
/* translators: %d: item sequence number. */
return sprintf( __( 'Demo summary %d with brief information.', 'documentate' ), $index );
}

if ( false !== strpos( $slug, 'objeto' ) ) {
return __( 'Subject of the example resolution to illustrate the workflow.', 'documentate' );
}

if ( false !== strpos( $slug, 'antecedentes' ) ) {
return __( 'Background facts written with test content.', 'documentate' );
}

if ( false !== strpos( $slug, 'fundamentos' ) ) {
return __( 'Legal grounds for testing with generic references.', 'documentate' );
}

if ( false !== strpos( $slug, 'resuelv' ) ) {
return '<p>' . __( 'First. Approve the demo action.', 'documentate' ) . '</p><p>' . __( 'Second. Notify interested parties.', 'documentate' ) . '</p>';
}

if ( false !== strpos( $slug, 'observaciones' ) ) {
return __( 'Additional observations to complete the template.', 'documentate' );
}

// Repeater "gastos" fields (table row repeater).
if ( false !== strpos( $slug, 'proveedor' ) ) {
return ( 1 === $index ) ? 'Suministros Ejemplo S.L.' : 'Servicios Demo S.A.';
}

if ( 'cif' === $slug ) {
return ( 1 === $index ) ? 'B12345678' : 'A87654321';
}

if ( false !== strpos( $slug, 'factura' ) ) {
return sprintf( '%03d/2025', 100 + $index );
}

if ( false !== strpos( $slug, 'importe' ) ) {
return ( 1 === $index ) ? '1250' : '3475.50';
}

// Fields for autorizacionviaje.odt template.
if ( false !== strpos( $slug, 'lugar' ) ) {
return 'Madrid';
}

if ( false !== strpos( $slug, 'invitante' ) ) {
return 'Ministerio de Educación';
}

if ( false !== strpos( $slug, 'temas' ) ) {
return 'Discusión de programas de innovación educativa y coordinación interterritorial.';
}

if ( false !== strpos( $slug, 'pagador' ) ) {
return 'Consejería de Educación del Gobierno de Canarias';
}

if ( false !== strpos( $slug, 'apellido1' ) ) {
return ( 1 === $index ) ? 'García' : 'Rodríguez';
}

if ( false !== strpos( $slug, 'apellido2' ) ) {
return ( 1 === $index ) ? 'López' : 'Martínez';
}

// Fields for gastossuplidos.odt template.
if ( false !== strpos( $slug, 'iban' ) ) {
return 'ES9121000418450200051332';
}

if ( false !== strpos( $slug, 'nombre_completo' ) ) {
return ( 1 === $index ) ? 'María García López' : 'Juan Rodríguez Martínez';
}

if ( false !== strpos( $slug, 'body' ) || false !== strpos( $slug, 'cuerpo' ) ) {
$rich = '<h3>' . __( 'Test heading', 'documentate' ) . '</h3>';
$rich .= '<p>' . __( 'First paragraph with example text.', 'documentate' ) . '</p>';
/* translators: 1: bold text label, 2: italic text label, 3: underline text label. */
$rich .= '<p>' . sprintf( __( 'Second paragraph with %1$s, %2$s and %3$s.', 'documentate' ), '<strong>' . __( 'bold', 'documentate' ) . '</strong>', '<em>' . __( 'italics', 'documentate' ) . '</em>', '<u>' . __( 'underline', 'documentate' ) . '</u>' ) . '</p>';
$rich .= '<ul><li>' . __( 'Item one', 'documentate' ) . '</li><li>' . __( 'Item two', 'documentate' ) . '</li></ul>';
$rich .= '<table><tr><th>' . __( 'Col 1', 'documentate' ) . '</th><th>' . __( 'Col 2', 'documentate' ) . '</th></tr><tr><td>' . __( 'Data A1', 'documentate' ) . '</td><td>' . __( 'Data A2', 'documentate' ) . '</td></tr><tr><td>' . __( 'Data B1', 'documentate' ) . '</td><td>' . __( 'Data B2', 'documentate' ) . '</td></tr></table>';
return $rich;
}

// Generic HTML content fields: enrich demo data with formatted HTML.
if (
false !== strpos( $slug, 'content' ) ||
false !== strpos( $slug, 'contenido' ) ||
false !== strpos( $slug, 'html' )
) {
$rich = '<h3>' . __( 'Test heading', 'documentate' ) . '</h3>';
$rich .= '<p>' . __( 'First paragraph with example text.', 'documentate' ) . '</p>';
/* translators: 1: bold text label, 2: italic text label, 3: underline text label. */
$rich .= '<p>' . sprintf( __( 'Second paragraph with %1$s, %2$s and %3$s.', 'documentate' ), '<strong>' . __( 'bold', 'documentate' ) . '</strong>', '<em>' . __( 'italics', 'documentate' ) . '</em>', '<u>' . __( 'underline', 'documentate' ) . '</u>' ) . '</p>';
$rich .= '<ul><li>' . __( 'Item one', 'documentate' ) . '</li><li>' . __( 'Item two', 'documentate' ) . '</li></ul>';
$rich .= '<table><tr><th>' . __( 'Col 1', 'documentate' ) . '</th><th>' . __( 'Col 2', 'documentate' ) . '</th></tr><tr><td>' . __( 'Data A1', 'documentate' ) . '</td><td>' . __( 'Data A2', 'documentate' ) . '</td></tr><tr><td>' . __( 'Data B1', 'documentate' ) . '</td><td>' . __( 'Data B2', 'documentate' ) . '</td></tr></table>';
return $rich;
}

if ( false !== strpos( $slug, 'keywords' ) || false !== strpos( $slug, 'palabras' ) ) {
return __( 'keywords, tags, demo', 'documentate' );
}

return __( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 'documentate' );
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method generate_demo_scalar_value() has an NPath complexity of 3715041853440. The configured NPath complexity threshold is 500.
Comment on lines +1413 to +1571
public static function generate_demo_scalar_value( $slug, $type, $data_type, $index = 1, $context = array() ) {
$slug = strtolower( (string) $slug );
$type = sanitize_key( $type );
$data_type = sanitize_key( $data_type );
$index = max( 1, absint( $index ) );

$document_title = isset( $context['document_title'] ) ? (string) $context['document_title'] : __( 'Demo resolution', 'documentate' );
$number_value = (string) ( 1 + $index );

if ( 'date' === $data_type ) {
$month = max( 1, min( 12, $index ) );
$day = max( 1, min( 28, 10 + $index ) );
return sprintf( '2025-%02d-%02d', $month, $day );
}

if ( 'number' === $data_type ) {
return $number_value;
}

if ( 'boolean' === $data_type ) {
return ( $index % 2 ) ? '1' : '0';
}

if ( false !== strpos( $slug, 'email' ) ) {
return 'demo' . $index . '@ejemplo.es';
}

if ( false !== strpos( $slug, 'phone' ) || false !== strpos( $slug, 'tel' ) ) {
return '+3460000000' . $index;
}

if ( false !== strpos( $slug, 'dni' ) ) {
return '1234567' . $index . 'A';
}

if ( false !== strpos( $slug, 'url' ) || false !== strpos( $slug, 'sitio' ) || false !== strpos( $slug, 'web' ) ) {
return 'https://ejemplo.es/recurso-' . $index;
}

if ( false !== strpos( $slug, 'nombre' ) || false !== strpos( $slug, 'name' ) ) {
return ( 1 === $index ) ? 'Jane Doe' : 'John Smith';
}

if ( false !== strpos( $slug, 'title' ) || false !== strpos( $slug, 'titulo' ) || 'post_title' === $slug ) {
if ( 'post_title' === $slug ) {
return $document_title;
}

/* translators: %d: item sequence number. */
return sprintf( __( 'Demo item %d', 'documentate' ), $index );
}

if ( false !== strpos( $slug, 'summary' ) || false !== strpos( $slug, 'resumen' ) ) {
/* translators: %d: item sequence number. */
return sprintf( __( 'Demo summary %d with brief information.', 'documentate' ), $index );
}

if ( false !== strpos( $slug, 'objeto' ) ) {
return __( 'Subject of the example resolution to illustrate the workflow.', 'documentate' );
}

if ( false !== strpos( $slug, 'antecedentes' ) ) {
return __( 'Background facts written with test content.', 'documentate' );
}

if ( false !== strpos( $slug, 'fundamentos' ) ) {
return __( 'Legal grounds for testing with generic references.', 'documentate' );
}

if ( false !== strpos( $slug, 'resuelv' ) ) {
return '<p>' . __( 'First. Approve the demo action.', 'documentate' ) . '</p><p>' . __( 'Second. Notify interested parties.', 'documentate' ) . '</p>';
}

if ( false !== strpos( $slug, 'observaciones' ) ) {
return __( 'Additional observations to complete the template.', 'documentate' );
}

// Repeater "gastos" fields (table row repeater).
if ( false !== strpos( $slug, 'proveedor' ) ) {
return ( 1 === $index ) ? 'Suministros Ejemplo S.L.' : 'Servicios Demo S.A.';
}

if ( 'cif' === $slug ) {
return ( 1 === $index ) ? 'B12345678' : 'A87654321';
}

if ( false !== strpos( $slug, 'factura' ) ) {
return sprintf( '%03d/2025', 100 + $index );
}

if ( false !== strpos( $slug, 'importe' ) ) {
return ( 1 === $index ) ? '1250' : '3475.50';
}

// Fields for autorizacionviaje.odt template.
if ( false !== strpos( $slug, 'lugar' ) ) {
return 'Madrid';
}

if ( false !== strpos( $slug, 'invitante' ) ) {
return 'Ministerio de Educación';
}

if ( false !== strpos( $slug, 'temas' ) ) {
return 'Discusión de programas de innovación educativa y coordinación interterritorial.';
}

if ( false !== strpos( $slug, 'pagador' ) ) {
return 'Consejería de Educación del Gobierno de Canarias';
}

if ( false !== strpos( $slug, 'apellido1' ) ) {
return ( 1 === $index ) ? 'García' : 'Rodríguez';
}

if ( false !== strpos( $slug, 'apellido2' ) ) {
return ( 1 === $index ) ? 'López' : 'Martínez';
}

// Fields for gastossuplidos.odt template.
if ( false !== strpos( $slug, 'iban' ) ) {
return 'ES9121000418450200051332';
}

if ( false !== strpos( $slug, 'nombre_completo' ) ) {
return ( 1 === $index ) ? 'María García López' : 'Juan Rodríguez Martínez';
}

if ( false !== strpos( $slug, 'body' ) || false !== strpos( $slug, 'cuerpo' ) ) {
$rich = '<h3>' . __( 'Test heading', 'documentate' ) . '</h3>';
$rich .= '<p>' . __( 'First paragraph with example text.', 'documentate' ) . '</p>';
/* translators: 1: bold text label, 2: italic text label, 3: underline text label. */
$rich .= '<p>' . sprintf( __( 'Second paragraph with %1$s, %2$s and %3$s.', 'documentate' ), '<strong>' . __( 'bold', 'documentate' ) . '</strong>', '<em>' . __( 'italics', 'documentate' ) . '</em>', '<u>' . __( 'underline', 'documentate' ) . '</u>' ) . '</p>';
$rich .= '<ul><li>' . __( 'Item one', 'documentate' ) . '</li><li>' . __( 'Item two', 'documentate' ) . '</li></ul>';
$rich .= '<table><tr><th>' . __( 'Col 1', 'documentate' ) . '</th><th>' . __( 'Col 2', 'documentate' ) . '</th></tr><tr><td>' . __( 'Data A1', 'documentate' ) . '</td><td>' . __( 'Data A2', 'documentate' ) . '</td></tr><tr><td>' . __( 'Data B1', 'documentate' ) . '</td><td>' . __( 'Data B2', 'documentate' ) . '</td></tr></table>';
return $rich;
}

// Generic HTML content fields: enrich demo data with formatted HTML.
if (
false !== strpos( $slug, 'content' ) ||
false !== strpos( $slug, 'contenido' ) ||
false !== strpos( $slug, 'html' )
) {
$rich = '<h3>' . __( 'Test heading', 'documentate' ) . '</h3>';
$rich .= '<p>' . __( 'First paragraph with example text.', 'documentate' ) . '</p>';
/* translators: 1: bold text label, 2: italic text label, 3: underline text label. */
$rich .= '<p>' . sprintf( __( 'Second paragraph with %1$s, %2$s and %3$s.', 'documentate' ), '<strong>' . __( 'bold', 'documentate' ) . '</strong>', '<em>' . __( 'italics', 'documentate' ) . '</em>', '<u>' . __( 'underline', 'documentate' ) . '</u>' ) . '</p>';
$rich .= '<ul><li>' . __( 'Item one', 'documentate' ) . '</li><li>' . __( 'Item two', 'documentate' ) . '</li></ul>';
$rich .= '<table><tr><th>' . __( 'Col 1', 'documentate' ) . '</th><th>' . __( 'Col 2', 'documentate' ) . '</th></tr><tr><td>' . __( 'Data A1', 'documentate' ) . '</td><td>' . __( 'Data A2', 'documentate' ) . '</td></tr><tr><td>' . __( 'Data B1', 'documentate' ) . '</td><td>' . __( 'Data B2', 'documentate' ) . '</td></tr></table>';
return $rich;
}

if ( false !== strpos( $slug, 'keywords' ) || false !== strpos( $slug, 'palabras' ) ) {
return __( 'keywords, tags, demo', 'documentate' );
}

return __( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 'documentate' );
}

Check warning

Code scanning / PHPMD

Code Size Rules: ExcessiveMethodLength Warning

The method generate_demo_scalar_value() has 159 lines of code. Current threshold is set to 150. Avoid really long methods.
Comment on lines +2384 to +2458
public function add_admin_filters( $post_type, $which ) {
if ( 'documentate_document' !== $post_type || 'top' !== $which ) {
return;
}

// Author filter.
$authors = get_users(
array(
'has_published_posts' => array( 'documentate_document' ),
'fields' => array( 'ID', 'display_name' ),
'orderby' => 'display_name',
)
);

if ( ! empty( $authors ) ) {
$current_author = isset( $_GET['author'] ) ? absint( $_GET['author'] ) : 0;
echo '<select name="author" id="filter-by-author">';
echo '<option value="">' . esc_html__( 'All authors', 'documentate' ) . '</option>';
foreach ( $authors as $author ) {
printf(
'<option value="%d"%s>%s</option>',
absint( $author->ID ),
selected( $current_author, $author->ID, false ),
esc_html( $author->display_name )
);
}
echo '</select>';
}

// Document type filter (taxonomy dropdown).
$doc_types = get_terms(
array(
'taxonomy' => 'documentate_doc_type',
'hide_empty' => false,
)
);

if ( ! is_wp_error( $doc_types ) && ! empty( $doc_types ) ) {
$current_type = isset( $_GET['documentate_doc_type'] ) ? sanitize_text_field( wp_unslash( $_GET['documentate_doc_type'] ) ) : '';
echo '<select name="documentate_doc_type" id="filter-by-doc-type">';
echo '<option value="">' . esc_html__( 'All document types', 'documentate' ) . '</option>';
foreach ( $doc_types as $doc_type ) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr( $doc_type->slug ),
selected( $current_type, $doc_type->slug, false ),
esc_html( $doc_type->name )
);
}
echo '</select>';
}

// Category filter (if taxonomy exists).
$categories = get_terms(
array(
'taxonomy' => 'category',
'hide_empty' => false,
)
);

if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) {
$current_cat = isset( $_GET['category_name'] ) ? sanitize_text_field( wp_unslash( $_GET['category_name'] ) ) : '';
echo '<select name="category_name" id="filter-by-category">';
echo '<option value="">' . esc_html__( 'All categories', 'documentate' ) . '</option>';
foreach ( $categories as $category ) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr( $category->slug ),
selected( $current_cat, $category->slug, false ),
esc_html( $category->name )
);
}
echo '</select>';
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method add_admin_filters() has an NPath complexity of 540. The configured NPath complexity threshold is 500.
Comment on lines +2466 to +2542
public function apply_admin_filters( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}

$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'edit-documentate_document' !== $screen->id ) {
return;
}

// Hide archived posts unless specifically requesting them.
$post_status = $query->get( 'post_status' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$show_archived = isset( $_GET['post_status'] ) && 'archived' === sanitize_key( $_GET['post_status'] );

if ( empty( $post_status ) && ! $show_archived ) {
// Default view: exclude archived.
$query->set( 'post_status', array( 'publish', 'pending', 'draft', 'private', 'future' ) );
}

$orderby = $query->get( 'orderby' );

// Handle sorting by author.
if ( 'author_name' === $orderby ) {
$query->set( 'orderby', 'author' );
}

// Handle sorting by document type.
if ( 'doc_type' === $orderby ) {
add_filter(
'posts_clauses',
function ( $clauses, $wp_query ) {
global $wpdb;

if ( $wp_query->get( 'orderby' ) !== 'doc_type' ) {
return $clauses;
}

$order = strtoupper( $wp_query->get( 'order' ) ) === 'ASC' ? 'ASC' : 'DESC';

$clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} AS dtr ON ({$wpdb->posts}.ID = dtr.object_id)";
$clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} AS dtt ON (dtr.term_taxonomy_id = dtt.term_taxonomy_id AND dtt.taxonomy = 'documentate_doc_type')";
$clauses['join'] .= " LEFT JOIN {$wpdb->terms} AS dt ON (dtt.term_id = dt.term_id)";
$clauses['orderby'] = "dt.name {$order}, " . $clauses['orderby'];

return $clauses;
},
10,
2
);
}

// Handle sorting by category.
if ( 'category_name' === $orderby ) {
add_filter(
'posts_clauses',
function ( $clauses, $wp_query ) {
global $wpdb;

if ( $wp_query->get( 'orderby' ) !== 'category_name' ) {
return $clauses;
}

$order = strtoupper( $wp_query->get( 'order' ) ) === 'ASC' ? 'ASC' : 'DESC';

$clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} AS ctr ON ({$wpdb->posts}.ID = ctr.object_id)";
$clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} AS ctt ON (ctr.term_taxonomy_id = ctt.term_taxonomy_id AND ctt.taxonomy = 'category')";
$clauses['join'] .= " LEFT JOIN {$wpdb->terms} AS ct ON (ctt.term_id = ct.term_id)";
$clauses['orderby'] = "ct.name {$order}, " . $clauses['orderby'];

return $clauses;
},
10,
2
);
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method apply_admin_filters() has a Cyclomatic Complexity of 16. The configured cyclomatic complexity threshold is 15.
Comment on lines +2466 to +2542
public function apply_admin_filters( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}

$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'edit-documentate_document' !== $screen->id ) {
return;
}

// Hide archived posts unless specifically requesting them.
$post_status = $query->get( 'post_status' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$show_archived = isset( $_GET['post_status'] ) && 'archived' === sanitize_key( $_GET['post_status'] );

if ( empty( $post_status ) && ! $show_archived ) {
// Default view: exclude archived.
$query->set( 'post_status', array( 'publish', 'pending', 'draft', 'private', 'future' ) );
}

$orderby = $query->get( 'orderby' );

// Handle sorting by author.
if ( 'author_name' === $orderby ) {
$query->set( 'orderby', 'author' );
}

// Handle sorting by document type.
if ( 'doc_type' === $orderby ) {
add_filter(
'posts_clauses',
function ( $clauses, $wp_query ) {
global $wpdb;

if ( $wp_query->get( 'orderby' ) !== 'doc_type' ) {
return $clauses;
}

$order = strtoupper( $wp_query->get( 'order' ) ) === 'ASC' ? 'ASC' : 'DESC';

$clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} AS dtr ON ({$wpdb->posts}.ID = dtr.object_id)";
$clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} AS dtt ON (dtr.term_taxonomy_id = dtt.term_taxonomy_id AND dtt.taxonomy = 'documentate_doc_type')";
$clauses['join'] .= " LEFT JOIN {$wpdb->terms} AS dt ON (dtt.term_id = dt.term_id)";
$clauses['orderby'] = "dt.name {$order}, " . $clauses['orderby'];

return $clauses;
},
10,
2
);
}

// Handle sorting by category.
if ( 'category_name' === $orderby ) {
add_filter(
'posts_clauses',
function ( $clauses, $wp_query ) {
global $wpdb;

if ( $wp_query->get( 'orderby' ) !== 'category_name' ) {
return $clauses;
}

$order = strtoupper( $wp_query->get( 'order' ) ) === 'ASC' ? 'ASC' : 'DESC';

$clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} AS ctr ON ({$wpdb->posts}.ID = ctr.object_id)";
$clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} AS ctt ON (ctr.term_taxonomy_id = ctt.term_taxonomy_id AND ctt.taxonomy = 'category')";
$clauses['join'] .= " LEFT JOIN {$wpdb->terms} AS ct ON (ctt.term_id = ct.term_id)";
$clauses['orderby'] = "ct.name {$order}, " . $clauses['orderby'];

return $clauses;
},
10,
2
);
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method apply_admin_filters() has an NPath complexity of 2700. The configured NPath complexity threshold is 500.
Copy link

@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.

Reviewed commit: 45df5ba4fc

ℹ️ 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 "@codex address that feedback".

Comment on lines -1 to -5
/**
* ZetaJS converter thread script.
* Runs inside the LibreOffice WASM web worker to handle document conversion.
*
* IMPORTANT: Files are written to the FS by the MAIN THREAD.

Choose a reason for hiding this comment

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

P1 Badge Restore converterThread.js or update worker URL

This commit deletes admin/vendor/zetajs/converterThread.js, but the converter popup still builds threadUrl that points to that file (admin/documentate-converter-template.php uses plugins_url('admin/vendor/zetajs/converterThread.js', ...)). A repo search (rg converterThread) now only finds that reference, so the worker script will 404 and ZetaJS conversions (preview/export in the converter popup) won’t start.

Useful? React with 👍 / 👎.

Comment on lines +224 to +228
if ( $post_id > 0 ) {
$current_post = get_post( $post_id );
if ( $current_post && 'publish' !== $current_post->post_status ) {
// Can only archive from publish.
$data['post_status'] = $current_post->post_status;

Choose a reason for hiding this comment

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

P2 Badge Block archiving when creating new posts

The archive guard only validates the previous status when $post_id > 0, so creating a new document with post_status=archived (e.g., via REST/CLI or programmatic wp_insert_post) bypasses the “only published documents can be archived” rule and even skips the doc‑type enforcement (Rule 1 doesn’t include archived). That allows brand‑new documents to be saved as archived, which contradicts the workflow intent and yields archived items that were never published.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants