Skip to content

Add better document flow#114

Merged
erseco merged 14 commits intomainfrom
improve-document-flow
Mar 4, 2026
Merged

Add better document flow#114
erseco merged 14 commits intomainfrom
improve-document-flow

Conversation

@erseco
Copy link
Copy Markdown
Collaborator

@erseco erseco commented Mar 4, 2026

This pull request introduces a major refactor of the document workflow UI and backend logic, streamlining the document management experience for both admins and editors. The legacy WordPress "submitdiv" is replaced with a custom Document Management meta box, and workflow actions (draft, review, publish, revert) are now handled by dedicated buttons. The code removes complex status restriction logic and legacy UI hiding, centralizing all workflow controls in the new meta box. End-to-end tests and CSS are updated to reflect these changes.

Workflow UI and Logic Refactor

  • Replaces the legacy WordPress "submitdiv" with a unified Document Management meta box, removing all related status restriction, visibility, and scheduling logic from documentate-workflow.js and backend code. All workflow actions (save draft, send to review, approve & publish, return to draft/review, save pending) are now handled by custom buttons, with backend hooks and CSS updated accordingly. [1] [2] [3] [4] [5] [6]

  • Removes legacy meta box controls and CSS hiding for submit box elements, including visibility options and scheduling UI, from the backend (class-documentate-documents.php). [1] [2] [3]

UI/UX Improvements

  • Adds a compact download row for document export buttons in the Document Management meta box, with improved styling via new CSS classes (documentate-download-row, documentate-download-label). [1] [2] [3]

  • Moves the actions meta box to a more prominent position ('core' context instead of 'high'), and updates the supported features for the custom post type to include comments. [1] [2]

End-to-End Test Updates

  • Refactors e2e page objects and fixtures to use the new workflow buttons and spinner selectors, ensuring tests interact with the custom meta box instead of legacy controls. Updates test specs to verify the removal of the submitdiv and the presence of the new management meta box. [1] [2] [3] [4] [5]

Workflow State Notices

  • Improves workflow state notices for pending review documents, ensuring editors receive clear feedback when a document is locked or pending review. [1] [2]

These changes modernize and simplify the document workflow, making it easier for users to manage document states and for developers to maintain the codebase.

Comment on lines +643 to +824
echo '<p>' . esc_html__('Insufficient permissions.', 'documentate') . '</p>';
return;
}

$nonce_export = wp_create_nonce( 'documentate_export_' . $post->ID );
$nonce_prev = wp_create_nonce( 'documentate_preview_' . $post->ID );
$nonce_export = wp_create_nonce('documentate_export_' . $post->ID);
$nonce_prev = wp_create_nonce('documentate_preview_' . $post->ID);

$preview = $this->build_action_url( 'documentate_preview', $post->ID, $nonce_prev );
$docx = $this->build_action_url( 'documentate_export_docx', $post->ID, $nonce_export );
$pdf = $this->build_action_url( 'documentate_export_pdf', $post->ID, $nonce_export );
$odt = $this->build_action_url( 'documentate_export_odt', $post->ID, $nonce_export );
$preview = $this->build_action_url('documentate_preview', $post->ID, $nonce_prev);
$docx = $this->build_action_url('documentate_export_docx', $post->ID, $nonce_export);
$pdf = $this->build_action_url('documentate_export_pdf', $post->ID, $nonce_export);
$odt = $this->build_action_url('documentate_export_odt', $post->ID, $nonce_export);

$this->ensure_document_generator();

$docx_template = Documentate_Document_Generator::get_template_path( $post->ID, 'docx' );
$odt_template = Documentate_Document_Generator::get_template_path( $post->ID, 'odt' );
$docx_template = Documentate_Document_Generator::get_template_path($post->ID, 'docx');
$odt_template = Documentate_Document_Generator::get_template_path($post->ID, 'odt');

require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-conversion-manager.php';
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-conversion-manager.php';

$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = ( '' === $docx_template && '' !== $odt_template );
$odt_requires_conversion = ( '' === $odt_template && '' !== $docx_template );
$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = '' === $docx_template && '' !== $odt_template;
$odt_requires_conversion = '' === $odt_template && '' !== $docx_template;

// Check if ZetaJS CDN mode is available for browser-based preview.
$zetajs_cdn_available = false;
if ( ! $conversion_ready ) {
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-zetajs-converter.php';
if (!$conversion_ready) {
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-zetajs-converter.php';
$zetajs_cdn_available = Documentate_Zetajs_Converter::is_cdn_mode();
}

// Check if we need popup-based conversion (bypasses PHP networking issues in Playground).
// This is needed for:
// 1. ZetaJS CDN mode (WASM conversion in browser)
// 2. Collabora in Playground (JavaScript fetch bypasses wp_remote_post multipart issues).
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground = Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground =
Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
$use_popup_for_conversion = $zetajs_cdn_available || $collabora_in_playground;

// In CDN mode or Playground with Collabora, browser can do conversions too.
$can_convert = $conversion_ready || $use_popup_for_conversion;
$docx_available = ( '' !== $docx_template ) || ( $docx_requires_conversion && $can_convert );
$odt_available = ( '' !== $odt_template ) || ( $odt_requires_conversion && $can_convert );
$pdf_available = $can_convert && ( '' !== $docx_template || '' !== $odt_template );
$docx_available = '' !== $docx_template || $docx_requires_conversion && $can_convert;
$odt_available = '' !== $odt_template || $odt_requires_conversion && $can_convert;
$pdf_available = $can_convert && ('' !== $docx_template || '' !== $odt_template);

// Determine source format for CDN conversions.
$source_format = '' !== $odt_template ? 'odt' : ( '' !== $docx_template ? 'docx' : '' );
$source_format = '' !== $odt_template ? 'odt' : ('' !== $docx_template ? 'docx' : '');

$docx_message = __( 'Configure a DOCX template in the document type.', 'documentate' );
if ( $docx_requires_conversion && ! $can_convert ) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message( 'odt', 'docx' );
$docx_message = __('Configure a DOCX template in the document type.', 'documentate');
if ($docx_requires_conversion && !$can_convert) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message('odt', 'docx');
}

$odt_message = __( 'Configure an ODT template in the document type.', 'documentate' );
if ( $odt_requires_conversion && ! $can_convert ) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message( 'docx', 'odt' );
$odt_message = __('Configure an ODT template in the document type.', 'documentate');
if ($odt_requires_conversion && !$can_convert) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message('docx', 'odt');
}

if ( '' === $docx_template && '' === $odt_template ) {
$pdf_message = __( 'Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate' );
} elseif ( ! $can_convert ) {
if ('' === $docx_template && '' === $odt_template) {
$pdf_message = __('Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate');
} elseif (!$can_convert) {
$source_for_pdf = '' !== $docx_template ? 'docx' : 'odt';
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message( $source_for_pdf, 'pdf' );
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message($source_for_pdf, 'pdf');
} else {
$pdf_message = '';
}

// Preview is available if server conversion is ready OR if popup conversion is available.
$preview_available = $pdf_available || ( $use_popup_for_conversion && ( '' !== $docx_template || '' !== $odt_template ) );
$preview_message = $pdf_message;
$preview_available = $pdf_available || $use_popup_for_conversion && ('' !== $docx_template || '' !== $odt_template);
$preview_message = $pdf_message;

$preferred_format = '';
$types = wp_get_post_terms( $post->ID, 'documentate_doc_type', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $types ) && ! empty( $types ) ) {
$type_id = intval( $types[0] );
$template_format = sanitize_key( (string) get_term_meta( $type_id, 'documentate_type_template_type', true ) );
if ( in_array( $template_format, array( 'docx', 'odt' ), true ) ) {
$types = wp_get_post_terms($post->ID, 'documentate_doc_type', array('fields' => 'ids'));
if (!is_wp_error($types) && !empty($types)) {
$type_id = intval($types[0]);
$template_format = sanitize_key((string) get_term_meta($type_id, 'documentate_type_template_type', true));
if (in_array($template_format, array('docx', 'odt'), true)) {
$preferred_format = $template_format;
}
}
if ( '' === $preferred_format ) {
if ( '' !== $docx_template ) {
if ('' === $preferred_format) {
if ('' !== $docx_template) {
$preferred_format = 'docx';
} elseif ( '' !== $odt_template ) {
} elseif ('' !== $odt_template) {
$preferred_format = 'odt';
}
}

echo '<p>';
if ( $preview_available ) {
// ── Primary row: Preview + Download PDF ──────────────────────────
$needs_popup_base = $zetajs_cdn_available && !$conversion_ready || $collabora_in_playground;

echo '<div class="documentate-actions-primary">';

// Preview button.
if ($preview_available) {
$preview_attrs = array(
'class' => 'button button-secondary documentate-action-btn',
'href' => '#',
'class' => 'button documentate-action-btn documentate-action-btn--preview',
'href' => '#',
'data-documentate-action' => 'preview',
'data-documentate-format' => 'pdf',
);
// Use popup for browser-based conversion:
// - ZetaJS CDN mode when no server conversion is available
// - Collabora in Playground (always, to bypass wp_remote_post multipart issues).
$needs_popup = ( $zetajs_cdn_available && ! $conversion_ready ) || $collabora_in_playground;
if ( $needs_popup ) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$needs_popup = $needs_popup_base;
if ($needs_popup) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$preview_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo '<a ' . $this->build_action_attributes( $preview_attrs ) . '>' . esc_html__( 'Preview', 'documentate' ) . '</a>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($preview_attrs)
. '><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</a>'
;
} else {
echo '<button type="button" class="button button-secondary" disabled title="' . esc_attr( $preview_message ) . '">' . esc_html__( 'Preview', 'documentate' ) . '</button>';
echo
'<button type="button" class="button documentate-action-btn--preview" disabled title="'
. esc_attr($preview_message)
. '"><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</button>'
;
}

// Download PDF button (primary/blue).
if ($pdf_available) {
$pdf_attrs = array(
'class' => 'button button-primary documentate-action-btn documentate-action-btn--pdf',
'href' => '#',
'data-documentate-action' => 'download',
'data-documentate-format' => 'pdf',
);
if ($needs_popup_base && 'pdf' !== $source_format) {
$pdf_attrs['data-documentate-cdn-mode'] = '1';
$pdf_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($pdf_attrs)
. '><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</a>'
;
} else {
echo
'<button type="button" class="button button-primary documentate-action-btn--pdf" disabled title="'
. esc_attr($pdf_message)
. '"><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</button>'
;
}
echo '</p>';

$buttons = array(
'docx' => array(
'href' => $docx,
'available' => $docx_available,
'message' => $docx_message,
'primary' => ( 'docx' === $preferred_format ),
'label' => 'DOCX',
),
'odt' => array(
'href' => $odt,
echo '</div>';

// ── Secondary row: Other download formats ────────────────────────
$secondary_buttons = array(
'odt' => array(
'available' => $odt_available,
'message' => $odt_message,
'primary' => ( 'odt' === $preferred_format ),
'label' => 'ODT',
'message' => $odt_message,
'label' => __('ODT (Source)', 'documentate'),
),
'pdf' => array(
'href' => $pdf,
'available' => $pdf_available,
'message' => $pdf_message,
'primary' => false,
'label' => 'PDF',
'docx' => array(
'available' => $docx_available,
'message' => $docx_message,
'label' => 'DOCX',
),
);

echo '<p>';
foreach ( array( 'docx', 'odt', 'pdf' ) as $format ) {
$data = $buttons[ $format ];
$class = $data['primary'] ? 'button button-primary documentate-action-btn' : 'button documentate-action-btn';
if ( $data['available'] ) {
echo '<div class="documentate-actions-secondary">';
echo
'<span class="documentate-actions-secondary__label">'
. esc_html__('Other download formats:', 'documentate')
. '</span>'
;
echo '<span class="documentate-actions-secondary__buttons">';
foreach ($secondary_buttons as $format => $data) {
if ($data['available']) {
$attrs = array(
'class' => $class,
'href' => '#',
'class' => 'button button-small documentate-action-btn',

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method render_actions_metabox() has a Cyclomatic Complexity of 51. The configured cyclomatic complexity threshold is 15.
Comment on lines +643 to +824
echo '<p>' . esc_html__('Insufficient permissions.', 'documentate') . '</p>';
return;
}

$nonce_export = wp_create_nonce( 'documentate_export_' . $post->ID );
$nonce_prev = wp_create_nonce( 'documentate_preview_' . $post->ID );
$nonce_export = wp_create_nonce('documentate_export_' . $post->ID);
$nonce_prev = wp_create_nonce('documentate_preview_' . $post->ID);

$preview = $this->build_action_url( 'documentate_preview', $post->ID, $nonce_prev );
$docx = $this->build_action_url( 'documentate_export_docx', $post->ID, $nonce_export );
$pdf = $this->build_action_url( 'documentate_export_pdf', $post->ID, $nonce_export );
$odt = $this->build_action_url( 'documentate_export_odt', $post->ID, $nonce_export );
$preview = $this->build_action_url('documentate_preview', $post->ID, $nonce_prev);
$docx = $this->build_action_url('documentate_export_docx', $post->ID, $nonce_export);
$pdf = $this->build_action_url('documentate_export_pdf', $post->ID, $nonce_export);
$odt = $this->build_action_url('documentate_export_odt', $post->ID, $nonce_export);

$this->ensure_document_generator();

$docx_template = Documentate_Document_Generator::get_template_path( $post->ID, 'docx' );
$odt_template = Documentate_Document_Generator::get_template_path( $post->ID, 'odt' );
$docx_template = Documentate_Document_Generator::get_template_path($post->ID, 'docx');
$odt_template = Documentate_Document_Generator::get_template_path($post->ID, 'odt');

require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-conversion-manager.php';
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-conversion-manager.php';

$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = ( '' === $docx_template && '' !== $odt_template );
$odt_requires_conversion = ( '' === $odt_template && '' !== $docx_template );
$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = '' === $docx_template && '' !== $odt_template;
$odt_requires_conversion = '' === $odt_template && '' !== $docx_template;

// Check if ZetaJS CDN mode is available for browser-based preview.
$zetajs_cdn_available = false;
if ( ! $conversion_ready ) {
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-zetajs-converter.php';
if (!$conversion_ready) {
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-zetajs-converter.php';
$zetajs_cdn_available = Documentate_Zetajs_Converter::is_cdn_mode();
}

// Check if we need popup-based conversion (bypasses PHP networking issues in Playground).
// This is needed for:
// 1. ZetaJS CDN mode (WASM conversion in browser)
// 2. Collabora in Playground (JavaScript fetch bypasses wp_remote_post multipart issues).
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground = Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground =
Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
$use_popup_for_conversion = $zetajs_cdn_available || $collabora_in_playground;

// In CDN mode or Playground with Collabora, browser can do conversions too.
$can_convert = $conversion_ready || $use_popup_for_conversion;
$docx_available = ( '' !== $docx_template ) || ( $docx_requires_conversion && $can_convert );
$odt_available = ( '' !== $odt_template ) || ( $odt_requires_conversion && $can_convert );
$pdf_available = $can_convert && ( '' !== $docx_template || '' !== $odt_template );
$docx_available = '' !== $docx_template || $docx_requires_conversion && $can_convert;
$odt_available = '' !== $odt_template || $odt_requires_conversion && $can_convert;
$pdf_available = $can_convert && ('' !== $docx_template || '' !== $odt_template);

// Determine source format for CDN conversions.
$source_format = '' !== $odt_template ? 'odt' : ( '' !== $docx_template ? 'docx' : '' );
$source_format = '' !== $odt_template ? 'odt' : ('' !== $docx_template ? 'docx' : '');

$docx_message = __( 'Configure a DOCX template in the document type.', 'documentate' );
if ( $docx_requires_conversion && ! $can_convert ) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message( 'odt', 'docx' );
$docx_message = __('Configure a DOCX template in the document type.', 'documentate');
if ($docx_requires_conversion && !$can_convert) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message('odt', 'docx');
}

$odt_message = __( 'Configure an ODT template in the document type.', 'documentate' );
if ( $odt_requires_conversion && ! $can_convert ) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message( 'docx', 'odt' );
$odt_message = __('Configure an ODT template in the document type.', 'documentate');
if ($odt_requires_conversion && !$can_convert) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message('docx', 'odt');
}

if ( '' === $docx_template && '' === $odt_template ) {
$pdf_message = __( 'Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate' );
} elseif ( ! $can_convert ) {
if ('' === $docx_template && '' === $odt_template) {
$pdf_message = __('Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate');
} elseif (!$can_convert) {
$source_for_pdf = '' !== $docx_template ? 'docx' : 'odt';
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message( $source_for_pdf, 'pdf' );
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message($source_for_pdf, 'pdf');
} else {
$pdf_message = '';
}

// Preview is available if server conversion is ready OR if popup conversion is available.
$preview_available = $pdf_available || ( $use_popup_for_conversion && ( '' !== $docx_template || '' !== $odt_template ) );
$preview_message = $pdf_message;
$preview_available = $pdf_available || $use_popup_for_conversion && ('' !== $docx_template || '' !== $odt_template);
$preview_message = $pdf_message;

$preferred_format = '';
$types = wp_get_post_terms( $post->ID, 'documentate_doc_type', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $types ) && ! empty( $types ) ) {
$type_id = intval( $types[0] );
$template_format = sanitize_key( (string) get_term_meta( $type_id, 'documentate_type_template_type', true ) );
if ( in_array( $template_format, array( 'docx', 'odt' ), true ) ) {
$types = wp_get_post_terms($post->ID, 'documentate_doc_type', array('fields' => 'ids'));
if (!is_wp_error($types) && !empty($types)) {
$type_id = intval($types[0]);
$template_format = sanitize_key((string) get_term_meta($type_id, 'documentate_type_template_type', true));
if (in_array($template_format, array('docx', 'odt'), true)) {
$preferred_format = $template_format;
}
}
if ( '' === $preferred_format ) {
if ( '' !== $docx_template ) {
if ('' === $preferred_format) {
if ('' !== $docx_template) {
$preferred_format = 'docx';
} elseif ( '' !== $odt_template ) {
} elseif ('' !== $odt_template) {
$preferred_format = 'odt';
}
}

echo '<p>';
if ( $preview_available ) {
// ── Primary row: Preview + Download PDF ──────────────────────────
$needs_popup_base = $zetajs_cdn_available && !$conversion_ready || $collabora_in_playground;

echo '<div class="documentate-actions-primary">';

// Preview button.
if ($preview_available) {
$preview_attrs = array(
'class' => 'button button-secondary documentate-action-btn',
'href' => '#',
'class' => 'button documentate-action-btn documentate-action-btn--preview',
'href' => '#',
'data-documentate-action' => 'preview',
'data-documentate-format' => 'pdf',
);
// Use popup for browser-based conversion:
// - ZetaJS CDN mode when no server conversion is available
// - Collabora in Playground (always, to bypass wp_remote_post multipart issues).
$needs_popup = ( $zetajs_cdn_available && ! $conversion_ready ) || $collabora_in_playground;
if ( $needs_popup ) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$needs_popup = $needs_popup_base;
if ($needs_popup) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$preview_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo '<a ' . $this->build_action_attributes( $preview_attrs ) . '>' . esc_html__( 'Preview', 'documentate' ) . '</a>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($preview_attrs)
. '><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</a>'
;
} else {
echo '<button type="button" class="button button-secondary" disabled title="' . esc_attr( $preview_message ) . '">' . esc_html__( 'Preview', 'documentate' ) . '</button>';
echo
'<button type="button" class="button documentate-action-btn--preview" disabled title="'
. esc_attr($preview_message)
. '"><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</button>'
;
}

// Download PDF button (primary/blue).
if ($pdf_available) {
$pdf_attrs = array(
'class' => 'button button-primary documentate-action-btn documentate-action-btn--pdf',
'href' => '#',
'data-documentate-action' => 'download',
'data-documentate-format' => 'pdf',
);
if ($needs_popup_base && 'pdf' !== $source_format) {
$pdf_attrs['data-documentate-cdn-mode'] = '1';
$pdf_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($pdf_attrs)
. '><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</a>'
;
} else {
echo
'<button type="button" class="button button-primary documentate-action-btn--pdf" disabled title="'
. esc_attr($pdf_message)
. '"><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</button>'
;
}
echo '</p>';

$buttons = array(
'docx' => array(
'href' => $docx,
'available' => $docx_available,
'message' => $docx_message,
'primary' => ( 'docx' === $preferred_format ),
'label' => 'DOCX',
),
'odt' => array(
'href' => $odt,
echo '</div>';

// ── Secondary row: Other download formats ────────────────────────
$secondary_buttons = array(
'odt' => array(
'available' => $odt_available,
'message' => $odt_message,
'primary' => ( 'odt' === $preferred_format ),
'label' => 'ODT',
'message' => $odt_message,
'label' => __('ODT (Source)', 'documentate'),
),
'pdf' => array(
'href' => $pdf,
'available' => $pdf_available,
'message' => $pdf_message,
'primary' => false,
'label' => 'PDF',
'docx' => array(
'available' => $docx_available,
'message' => $docx_message,
'label' => 'DOCX',
),
);

echo '<p>';
foreach ( array( 'docx', 'odt', 'pdf' ) as $format ) {
$data = $buttons[ $format ];
$class = $data['primary'] ? 'button button-primary documentate-action-btn' : 'button documentate-action-btn';
if ( $data['available'] ) {
echo '<div class="documentate-actions-secondary">';
echo
'<span class="documentate-actions-secondary__label">'
. esc_html__('Other download formats:', 'documentate')
. '</span>'
;
echo '<span class="documentate-actions-secondary__buttons">';
foreach ($secondary_buttons as $format => $data) {
if ($data['available']) {
$attrs = array(
'class' => $class,
'href' => '#',
'class' => 'button button-small documentate-action-btn',

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method render_actions_metabox() has an NPath complexity of 18247680. The configured NPath complexity threshold is 500.
Comment on lines +643 to +824
echo '<p>' . esc_html__('Insufficient permissions.', 'documentate') . '</p>';
return;
}

$nonce_export = wp_create_nonce( 'documentate_export_' . $post->ID );
$nonce_prev = wp_create_nonce( 'documentate_preview_' . $post->ID );
$nonce_export = wp_create_nonce('documentate_export_' . $post->ID);
$nonce_prev = wp_create_nonce('documentate_preview_' . $post->ID);

$preview = $this->build_action_url( 'documentate_preview', $post->ID, $nonce_prev );
$docx = $this->build_action_url( 'documentate_export_docx', $post->ID, $nonce_export );
$pdf = $this->build_action_url( 'documentate_export_pdf', $post->ID, $nonce_export );
$odt = $this->build_action_url( 'documentate_export_odt', $post->ID, $nonce_export );
$preview = $this->build_action_url('documentate_preview', $post->ID, $nonce_prev);
$docx = $this->build_action_url('documentate_export_docx', $post->ID, $nonce_export);
$pdf = $this->build_action_url('documentate_export_pdf', $post->ID, $nonce_export);
$odt = $this->build_action_url('documentate_export_odt', $post->ID, $nonce_export);

$this->ensure_document_generator();

$docx_template = Documentate_Document_Generator::get_template_path( $post->ID, 'docx' );
$odt_template = Documentate_Document_Generator::get_template_path( $post->ID, 'odt' );
$docx_template = Documentate_Document_Generator::get_template_path($post->ID, 'docx');
$odt_template = Documentate_Document_Generator::get_template_path($post->ID, 'odt');

require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-conversion-manager.php';
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-conversion-manager.php';

$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = ( '' === $docx_template && '' !== $odt_template );
$odt_requires_conversion = ( '' === $odt_template && '' !== $docx_template );
$conversion_ready = Documentate_Conversion_Manager::is_available();
$engine_label = Documentate_Conversion_Manager::get_engine_label();
$docx_requires_conversion = '' === $docx_template && '' !== $odt_template;
$odt_requires_conversion = '' === $odt_template && '' !== $docx_template;

// Check if ZetaJS CDN mode is available for browser-based preview.
$zetajs_cdn_available = false;
if ( ! $conversion_ready ) {
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-zetajs-converter.php';
if (!$conversion_ready) {
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-zetajs-converter.php';
$zetajs_cdn_available = Documentate_Zetajs_Converter::is_cdn_mode();
}

// Check if we need popup-based conversion (bypasses PHP networking issues in Playground).
// This is needed for:
// 1. ZetaJS CDN mode (WASM conversion in browser)
// 2. Collabora in Playground (JavaScript fetch bypasses wp_remote_post multipart issues).
require_once plugin_dir_path( __DIR__ ) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground = Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
require_once plugin_dir_path(__DIR__) . 'includes/class-documentate-collabora-converter.php';
$collabora_in_playground =
Documentate_Collabora_Converter::is_playground() && Documentate_Collabora_Converter::is_available();
$use_popup_for_conversion = $zetajs_cdn_available || $collabora_in_playground;

// In CDN mode or Playground with Collabora, browser can do conversions too.
$can_convert = $conversion_ready || $use_popup_for_conversion;
$docx_available = ( '' !== $docx_template ) || ( $docx_requires_conversion && $can_convert );
$odt_available = ( '' !== $odt_template ) || ( $odt_requires_conversion && $can_convert );
$pdf_available = $can_convert && ( '' !== $docx_template || '' !== $odt_template );
$docx_available = '' !== $docx_template || $docx_requires_conversion && $can_convert;
$odt_available = '' !== $odt_template || $odt_requires_conversion && $can_convert;
$pdf_available = $can_convert && ('' !== $docx_template || '' !== $odt_template);

// Determine source format for CDN conversions.
$source_format = '' !== $odt_template ? 'odt' : ( '' !== $docx_template ? 'docx' : '' );
$source_format = '' !== $odt_template ? 'odt' : ('' !== $docx_template ? 'docx' : '');

$docx_message = __( 'Configure a DOCX template in the document type.', 'documentate' );
if ( $docx_requires_conversion && ! $can_convert ) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message( 'odt', 'docx' );
$docx_message = __('Configure a DOCX template in the document type.', 'documentate');
if ($docx_requires_conversion && !$can_convert) {
$docx_message = Documentate_Conversion_Manager::get_unavailable_message('odt', 'docx');
}

$odt_message = __( 'Configure an ODT template in the document type.', 'documentate' );
if ( $odt_requires_conversion && ! $can_convert ) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message( 'docx', 'odt' );
$odt_message = __('Configure an ODT template in the document type.', 'documentate');
if ($odt_requires_conversion && !$can_convert) {
$odt_message = Documentate_Conversion_Manager::get_unavailable_message('docx', 'odt');
}

if ( '' === $docx_template && '' === $odt_template ) {
$pdf_message = __( 'Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate' );
} elseif ( ! $can_convert ) {
if ('' === $docx_template && '' === $odt_template) {
$pdf_message = __('Configure a DOCX or ODT template in the document type before generating PDF.', 'documentate');
} elseif (!$can_convert) {
$source_for_pdf = '' !== $docx_template ? 'docx' : 'odt';
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message( $source_for_pdf, 'pdf' );
$pdf_message = Documentate_Conversion_Manager::get_unavailable_message($source_for_pdf, 'pdf');
} else {
$pdf_message = '';
}

// Preview is available if server conversion is ready OR if popup conversion is available.
$preview_available = $pdf_available || ( $use_popup_for_conversion && ( '' !== $docx_template || '' !== $odt_template ) );
$preview_message = $pdf_message;
$preview_available = $pdf_available || $use_popup_for_conversion && ('' !== $docx_template || '' !== $odt_template);
$preview_message = $pdf_message;

$preferred_format = '';
$types = wp_get_post_terms( $post->ID, 'documentate_doc_type', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $types ) && ! empty( $types ) ) {
$type_id = intval( $types[0] );
$template_format = sanitize_key( (string) get_term_meta( $type_id, 'documentate_type_template_type', true ) );
if ( in_array( $template_format, array( 'docx', 'odt' ), true ) ) {
$types = wp_get_post_terms($post->ID, 'documentate_doc_type', array('fields' => 'ids'));
if (!is_wp_error($types) && !empty($types)) {
$type_id = intval($types[0]);
$template_format = sanitize_key((string) get_term_meta($type_id, 'documentate_type_template_type', true));
if (in_array($template_format, array('docx', 'odt'), true)) {
$preferred_format = $template_format;
}
}
if ( '' === $preferred_format ) {
if ( '' !== $docx_template ) {
if ('' === $preferred_format) {
if ('' !== $docx_template) {
$preferred_format = 'docx';
} elseif ( '' !== $odt_template ) {
} elseif ('' !== $odt_template) {
$preferred_format = 'odt';
}
}

echo '<p>';
if ( $preview_available ) {
// ── Primary row: Preview + Download PDF ──────────────────────────
$needs_popup_base = $zetajs_cdn_available && !$conversion_ready || $collabora_in_playground;

echo '<div class="documentate-actions-primary">';

// Preview button.
if ($preview_available) {
$preview_attrs = array(
'class' => 'button button-secondary documentate-action-btn',
'href' => '#',
'class' => 'button documentate-action-btn documentate-action-btn--preview',
'href' => '#',
'data-documentate-action' => 'preview',
'data-documentate-format' => 'pdf',
);
// Use popup for browser-based conversion:
// - ZetaJS CDN mode when no server conversion is available
// - Collabora in Playground (always, to bypass wp_remote_post multipart issues).
$needs_popup = ( $zetajs_cdn_available && ! $conversion_ready ) || $collabora_in_playground;
if ( $needs_popup ) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$needs_popup = $needs_popup_base;
if ($needs_popup) {
$preview_attrs['data-documentate-cdn-mode'] = '1';
$preview_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo '<a ' . $this->build_action_attributes( $preview_attrs ) . '>' . esc_html__( 'Preview', 'documentate' ) . '</a>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($preview_attrs)
. '><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</a>'
;
} else {
echo '<button type="button" class="button button-secondary" disabled title="' . esc_attr( $preview_message ) . '">' . esc_html__( 'Preview', 'documentate' ) . '</button>';
echo
'<button type="button" class="button documentate-action-btn--preview" disabled title="'
. esc_attr($preview_message)
. '"><span class="dashicons dashicons-visibility"></span> '
. esc_html__('Preview', 'documentate')
. '</button>'
;
}

// Download PDF button (primary/blue).
if ($pdf_available) {
$pdf_attrs = array(
'class' => 'button button-primary documentate-action-btn documentate-action-btn--pdf',
'href' => '#',
'data-documentate-action' => 'download',
'data-documentate-format' => 'pdf',
);
if ($needs_popup_base && 'pdf' !== $source_format) {
$pdf_attrs['data-documentate-cdn-mode'] = '1';
$pdf_attrs['data-documentate-source-format'] = $source_format;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes sanitized in build_action_attributes().
echo
'<a '
. $this->build_action_attributes($pdf_attrs)
. '><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</a>'
;
} else {
echo
'<button type="button" class="button button-primary documentate-action-btn--pdf" disabled title="'
. esc_attr($pdf_message)
. '"><span class="dashicons dashicons-pdf"></span> '
. esc_html__('Download PDF', 'documentate')
. '</button>'
;
}
echo '</p>';

$buttons = array(
'docx' => array(
'href' => $docx,
'available' => $docx_available,
'message' => $docx_message,
'primary' => ( 'docx' === $preferred_format ),
'label' => 'DOCX',
),
'odt' => array(
'href' => $odt,
echo '</div>';

// ── Secondary row: Other download formats ────────────────────────
$secondary_buttons = array(
'odt' => array(
'available' => $odt_available,
'message' => $odt_message,
'primary' => ( 'odt' === $preferred_format ),
'label' => 'ODT',
'message' => $odt_message,
'label' => __('ODT (Source)', 'documentate'),
),
'pdf' => array(
'href' => $pdf,
'available' => $pdf_available,
'message' => $pdf_message,
'primary' => false,
'label' => 'PDF',
'docx' => array(
'available' => $docx_available,
'message' => $docx_message,
'label' => 'DOCX',
),
);

echo '<p>';
foreach ( array( 'docx', 'odt', 'pdf' ) as $format ) {
$data = $buttons[ $format ];
$class = $data['primary'] ? 'button button-primary documentate-action-btn' : 'button documentate-action-btn';
if ( $data['available'] ) {
echo '<div class="documentate-actions-secondary">';
echo
'<span class="documentate-actions-secondary__label">'
. esc_html__('Other download formats:', 'documentate')
. '</span>'
;
echo '<span class="documentate-actions-secondary__buttons">';
foreach ($secondary_buttons as $format => $data) {
if ($data['available']) {
$attrs = array(
'class' => $class,
'href' => '#',
'class' => 'button button-small documentate-action-btn',

Check warning

Code scanning / PHPMD

Code Size Rules: ExcessiveMethodLength Warning

The method render_actions_metabox() has 241 lines of code. Current threshold is set to 150. Avoid really long methods.
Comment on lines 293 to 471
*/
private static function build_merge_fields( $post_id ) {
private static function build_merge_fields($post_id) {
self::reset_rich_field_values();
$opts = get_option( 'documentate_settings', array() );
$post = get_post( $post_id );
$opts = get_option('documentate_settings', array());
$post = get_post($post_id);
$structured = array();
if ( $post && class_exists( 'Documentate_Documents' ) ) {
$content = get_post_field( 'post_content', $post_id, 'raw' );
if ( ! is_string( $content ) || '' === $content ) {
if ($post && class_exists('Documentate_Documents')) {
$content = get_post_field('post_content', $post_id, 'raw');
if (!is_string($content) || '' === $content) {
$content = $post->post_content;
}
$structured = Documentate_Documents::parse_structured_content( (string) $content );
$structured = Documentate_Documents::parse_structured_content((string) $content);
}

// Apply case transformation to title based on schema attribute.
$title = get_post_field( 'post_title', $post_id, 'raw' );
$title_case = self::get_title_case_from_schema( $post_id );
$title = self::apply_case_transformation( $title, $title_case );
$title = get_post_field('post_title', $post_id, 'raw');
$title_case = self::get_title_case_from_schema($post_id);
$title = self::apply_case_transformation($title, $title_case);

$fields = array(
'title' => $title,
'title' => $title,
'post_title' => $title, // Alias for templates using [post_title].
'margen' => wp_strip_all_tags( isset( $opts['doc_margin_text'] ) ? $opts['doc_margin_text'] : '' ),
'margen' => wp_strip_all_tags(isset($opts['doc_margin_text']) ? $opts['doc_margin_text'] : ''),
);

$types = wp_get_post_terms( $post_id, 'documentate_doc_type', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $types ) && ! empty( $types ) ) {
$type_id = intval( $types[0] );
$schema = array();
if ( class_exists( 'Documentate_Documents' ) ) {
$schema = Documentate_Documents::get_term_schema( $type_id );
$types = wp_get_post_terms($post_id, 'documentate_doc_type', array('fields' => 'ids'));
if (!is_wp_error($types) && !empty($types)) {
$type_id = intval($types[0]);
$schema = array();
if (class_exists('Documentate_Documents')) {
$schema = Documentate_Documents::get_term_schema($type_id);
} else {
$schema = self::get_type_schema( $type_id );
$schema = self::get_type_schema($type_id);
}
foreach ( $schema as $def ) {
if ( empty( $def['slug'] ) ) {
continue;
foreach ($schema as $def) {
if (empty($def['slug'])) {
continue;
}
$slug = sanitize_key( $def['slug'] );
$slug = sanitize_key($def['slug']);

// Skip post_title - it's already set from get_the_title() above.
if ( 'post_title' === $slug ) {
if ('post_title' === $slug) {
continue;
}
// Prefer the original template name for TinyButStrong merges when available.
$tbs_name = '';
if ( isset( $def['name'] ) && is_string( $def['name'] ) ) {
$tbs_name = self::sanitize_placeholder_name( $def['name'] );
// Prefer the original template name for TinyButStrong merges when available.
$tbs_name = '';
if (isset($def['name']) && is_string($def['name'])) {
$tbs_name = self::sanitize_placeholder_name($def['name']);
}
if ( '' === $tbs_name ) {
$tbs_name = self::sanitize_placeholder_name( $slug );
if ('' === $tbs_name) {
$tbs_name = self::sanitize_placeholder_name($slug);
}

// Keep the legacy key used by UI (placeholder or slug) as alias for backward compatibility.
$alias_key = '';
if ( isset( $def['placeholder'] ) && is_string( $def['placeholder'] ) ) {
$alias_key = self::sanitize_placeholder_name( $def['placeholder'] );
// Keep the legacy key used by UI (placeholder or slug) as alias for backward compatibility.
$alias_key = '';
if (isset($def['placeholder']) && is_string($def['placeholder'])) {
$alias_key = self::sanitize_placeholder_name($def['placeholder']);
}
if ( '' === $alias_key ) {
$alias_key = self::sanitize_placeholder_name( $slug );
if ('' === $alias_key) {
$alias_key = self::sanitize_placeholder_name($slug);
}
$data_type = isset( $def['data_type'] ) ? sanitize_key( $def['data_type'] ) : 'text';
$type = isset( $def['type'] ) ? sanitize_key( $def['type'] ) : 'textarea';
$data_type = isset($def['data_type']) ? sanitize_key($def['data_type']) : 'text';
$type = isset($def['type']) ? sanitize_key($def['type']) : 'textarea';

if ( 'array' === $type ) {
$items = self::get_array_field_items_for_merge( $structured, $slug, $post_id );
if ('array' === $type) {
$items = self::get_array_field_items_for_merge($structured, $slug, $post_id);

// Apply case transformations to repeater items.
$item_schema = isset( $def['item_schema'] ) ? $def['item_schema'] : array();
$items = self::apply_case_to_array_items( $items, $item_schema );
$item_schema = isset($def['item_schema']) ? $def['item_schema'] : array();
$items = self::apply_case_to_array_items($items, $item_schema);

// Use block name for MergeBlock, with alias for legacy behavior.
$fields[ $tbs_name ] = $items;
if ( $alias_key !== $tbs_name ) {
$fields[ $alias_key ] = $items;
$fields[$tbs_name] = $items;
if ($alias_key !== $tbs_name) {
$fields[$alias_key] = $items;
}
self::remember_rich_values_from_array_items( $items );
self::remember_rich_values_from_array_items($items);
continue;
}

$value = self::get_structured_field_value( $structured, $slug, $post_id );
$value = self::get_structured_field_value($structured, $slug, $post_id);
// Force rich type if value contains block HTML, BEFORE prepare strips tags.
$original_type = $type;
if ( ! in_array( $type, array( 'rich', 'html' ), true ) && Documents_Meta_Handler::value_contains_block_html( $value ) ) {
if (!in_array($type, array('rich', 'html'), true) && Documents_Meta_Handler::value_contains_block_html($value)) {
$type = 'rich';
}
$prepared = self::prepare_field_value( $value, $type, $data_type, $def );
$prepared = self::prepare_field_value($value, $type, $data_type, $def);

// Apply case transformation if specified (skip for HTML content).
$field_case = isset( $def['case'] ) ? sanitize_key( $def['case'] ) : '';
if ( '' !== $field_case && ! in_array( $type, array( 'rich', 'html' ), true ) ) {
$prepared = self::apply_case_transformation( $prepared, $field_case );
$field_case = isset($def['case']) ? sanitize_key($def['case']) : '';
if ('' !== $field_case && !in_array($type, array('rich', 'html'), true)) {
$prepared = self::apply_case_transformation($prepared, $field_case);
}

$fields[ $tbs_name ] = $prepared;
if ( $alias_key !== $tbs_name ) {
$fields[ $alias_key ] = $prepared;
$fields[$tbs_name] = $prepared;
if ($alias_key !== $tbs_name) {
$fields[$alias_key] = $prepared;
}
// Register for rich text conversion if typed as rich/html.
// Also check if prepared value still contains HTML as a safety net.
$has_html_in_prepared = Documents_Meta_Handler::value_contains_block_html( $prepared );
if ( in_array( $type, array( 'rich', 'html' ), true ) || $has_html_in_prepared ) {
self::remember_rich_field_value( $prepared );
$has_html_in_prepared = Documents_Meta_Handler::value_contains_block_html($prepared);
if (in_array($type, array('rich', 'html'), true) || $has_html_in_prepared) {
self::remember_rich_field_value($prepared);
}
// Debug logging - enable with WP_DEBUG.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log(
sprintf(
'DOCUMENTATE [%s]: schema_type=%s, effective_type=%s, raw_has_html=%s, prepared_has_html=%s, raw_len=%d, prep_len=%d',
$slug,
$original_type,
$type,
Documents_Meta_Handler::value_contains_block_html( $value ) ? 'YES' : 'NO',
$has_html_in_prepared ? 'YES' : 'NO',
strlen( $value ),
strlen( $prepared )
)
);
if ( Documents_Meta_Handler::value_contains_block_html( $value ) && strlen( $value ) < 500 ) {
error_log( 'DOCUMENTATE [' . $slug . '] RAW: ' . substr( $value, 0, 300 ) );
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf(
'DOCUMENTATE [%s]: schema_type=%s, effective_type=%s, raw_has_html=%s, prepared_has_html=%s, raw_len=%d, prep_len=%d',
$slug,
$original_type,
$type,
Documents_Meta_Handler::value_contains_block_html($value) ? 'YES' : 'NO',
$has_html_in_prepared ? 'YES' : 'NO',
strlen($value),
strlen($prepared),
));
if (Documents_Meta_Handler::value_contains_block_html($value) && strlen($value) < 500) {
error_log('DOCUMENTATE [' . $slug . '] RAW: ' . substr($value, 0, 300));
}
}
}

$logos = get_term_meta( $type_id, 'documentate_type_logos', true );
if ( is_array( $logos ) && ! empty( $logos ) ) {
$i = 1;
foreach ( $logos as $att_id ) {
$att_id = intval( $att_id );
if ( $att_id <= 0 ) {
continue;
$logos = get_term_meta($type_id, 'documentate_type_logos', true);
if (is_array($logos) && !empty($logos)) {
$i = 1;
foreach ($logos as $att_id) {
$att_id = intval($att_id);
if ($att_id <= 0) {
continue;
}
$fields[ 'logo' . $i . '_path' ] = get_attached_file( $att_id );
$fields[ 'logo' . $i . '_url' ] = wp_get_attachment_url( $att_id );
$i++;
$fields['logo' . $i . '_path'] = get_attached_file($att_id);
$fields['logo' . $i . '_url'] = wp_get_attachment_url($att_id);
$i++;
}
}
}

if ( ! empty( $structured ) ) {
foreach ( $structured as $slug => $info ) {
$slug = sanitize_key( $slug );
if ( '' === $slug ) {
continue;
if (!empty($structured)) {
foreach ($structured as $slug => $info) {
$slug = sanitize_key($slug);
if ('' === $slug) {
continue;
}
$placeholder = $slug;
if ( isset( $fields[ $placeholder ] ) && '' !== $fields[ $placeholder ] ) {
continue;
$placeholder = $slug;
if (isset($fields[$placeholder]) && '' !== $fields[$placeholder]) {
continue;
}
if ( isset( $info['type'] ) && 'array' === sanitize_key( $info['type'] ) ) {
$items = self::get_array_field_items_for_merge( $structured, $slug, $post_id );
$fields[ $placeholder ] = $items;
self::remember_rich_values_from_array_items( $items );
continue;
if (isset($info['type']) && 'array' === sanitize_key($info['type'])) {
$items = self::get_array_field_items_for_merge($structured, $slug, $post_id);
$fields[$placeholder] = $items;
self::remember_rich_values_from_array_items($items);
continue;
}

$value = '';
if ( isset( $info['value'] ) ) {
$value = (string) $info['value'];
$value = '';
if (isset($info['value'])) {
$value = (string) $info['value'];
}
if ( '' === $value ) {
$value = self::get_structured_field_value( $structured, $slug, $post_id );
if ('' === $value) {
$value = self::get_structured_field_value($structured, $slug, $post_id);
}
$field_type = isset( $info['type'] ) ? sanitize_key( $info['type'] ) : 'rich';
$field_type = isset($info['type']) ? sanitize_key($info['type']) : 'rich';
// Force rich type if value contains block HTML, BEFORE prepare strips tags.
if ( ! in_array( $field_type, array( 'rich', 'html' ), true ) && Documents_Meta_Handler::value_contains_block_html( $value ) ) {
if (
!in_array($field_type, array('rich', 'html'), true) && Documents_Meta_Handler::value_contains_block_html($value)
) {
$field_type = 'rich';
}
$fields[ $placeholder ] = self::prepare_field_value( $value, $field_type, 'text' );
$fields[$placeholder] = self::prepare_field_value($value, $field_type, 'text');
// Register for rich text conversion if typed as rich/html.
// Also check if prepared value still contains HTML as a safety net.
if ( in_array( $field_type, array( 'rich', 'html' ), true ) || Documents_Meta_Handler::value_contains_block_html( $fields[ $placeholder ] ) ) {
self::remember_rich_field_value( $fields[ $placeholder ] );
if (
in_array($field_type, array('rich', 'html'), true)
|| Documents_Meta_Handler::value_contains_block_html($fields[$placeholder])
) {
self::remember_rich_field_value($fields[$placeholder]);
}
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method build_merge_fields() has a Cyclomatic Complexity of 56. The configured cyclomatic complexity threshold is 15.
Comment on lines 293 to 471
*/
private static function build_merge_fields( $post_id ) {
private static function build_merge_fields($post_id) {
self::reset_rich_field_values();
$opts = get_option( 'documentate_settings', array() );
$post = get_post( $post_id );
$opts = get_option('documentate_settings', array());
$post = get_post($post_id);
$structured = array();
if ( $post && class_exists( 'Documentate_Documents' ) ) {
$content = get_post_field( 'post_content', $post_id, 'raw' );
if ( ! is_string( $content ) || '' === $content ) {
if ($post && class_exists('Documentate_Documents')) {
$content = get_post_field('post_content', $post_id, 'raw');
if (!is_string($content) || '' === $content) {
$content = $post->post_content;
}
$structured = Documentate_Documents::parse_structured_content( (string) $content );
$structured = Documentate_Documents::parse_structured_content((string) $content);
}

// Apply case transformation to title based on schema attribute.
$title = get_post_field( 'post_title', $post_id, 'raw' );
$title_case = self::get_title_case_from_schema( $post_id );
$title = self::apply_case_transformation( $title, $title_case );
$title = get_post_field('post_title', $post_id, 'raw');
$title_case = self::get_title_case_from_schema($post_id);
$title = self::apply_case_transformation($title, $title_case);

$fields = array(
'title' => $title,
'title' => $title,
'post_title' => $title, // Alias for templates using [post_title].
'margen' => wp_strip_all_tags( isset( $opts['doc_margin_text'] ) ? $opts['doc_margin_text'] : '' ),
'margen' => wp_strip_all_tags(isset($opts['doc_margin_text']) ? $opts['doc_margin_text'] : ''),
);

$types = wp_get_post_terms( $post_id, 'documentate_doc_type', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $types ) && ! empty( $types ) ) {
$type_id = intval( $types[0] );
$schema = array();
if ( class_exists( 'Documentate_Documents' ) ) {
$schema = Documentate_Documents::get_term_schema( $type_id );
$types = wp_get_post_terms($post_id, 'documentate_doc_type', array('fields' => 'ids'));
if (!is_wp_error($types) && !empty($types)) {
$type_id = intval($types[0]);
$schema = array();
if (class_exists('Documentate_Documents')) {
$schema = Documentate_Documents::get_term_schema($type_id);
} else {
$schema = self::get_type_schema( $type_id );
$schema = self::get_type_schema($type_id);
}
foreach ( $schema as $def ) {
if ( empty( $def['slug'] ) ) {
continue;
foreach ($schema as $def) {
if (empty($def['slug'])) {
continue;
}
$slug = sanitize_key( $def['slug'] );
$slug = sanitize_key($def['slug']);

// Skip post_title - it's already set from get_the_title() above.
if ( 'post_title' === $slug ) {
if ('post_title' === $slug) {
continue;
}
// Prefer the original template name for TinyButStrong merges when available.
$tbs_name = '';
if ( isset( $def['name'] ) && is_string( $def['name'] ) ) {
$tbs_name = self::sanitize_placeholder_name( $def['name'] );
// Prefer the original template name for TinyButStrong merges when available.
$tbs_name = '';
if (isset($def['name']) && is_string($def['name'])) {
$tbs_name = self::sanitize_placeholder_name($def['name']);
}
if ( '' === $tbs_name ) {
$tbs_name = self::sanitize_placeholder_name( $slug );
if ('' === $tbs_name) {
$tbs_name = self::sanitize_placeholder_name($slug);
}

// Keep the legacy key used by UI (placeholder or slug) as alias for backward compatibility.
$alias_key = '';
if ( isset( $def['placeholder'] ) && is_string( $def['placeholder'] ) ) {
$alias_key = self::sanitize_placeholder_name( $def['placeholder'] );
// Keep the legacy key used by UI (placeholder or slug) as alias for backward compatibility.
$alias_key = '';
if (isset($def['placeholder']) && is_string($def['placeholder'])) {
$alias_key = self::sanitize_placeholder_name($def['placeholder']);
}
if ( '' === $alias_key ) {
$alias_key = self::sanitize_placeholder_name( $slug );
if ('' === $alias_key) {
$alias_key = self::sanitize_placeholder_name($slug);
}
$data_type = isset( $def['data_type'] ) ? sanitize_key( $def['data_type'] ) : 'text';
$type = isset( $def['type'] ) ? sanitize_key( $def['type'] ) : 'textarea';
$data_type = isset($def['data_type']) ? sanitize_key($def['data_type']) : 'text';
$type = isset($def['type']) ? sanitize_key($def['type']) : 'textarea';

if ( 'array' === $type ) {
$items = self::get_array_field_items_for_merge( $structured, $slug, $post_id );
if ('array' === $type) {
$items = self::get_array_field_items_for_merge($structured, $slug, $post_id);

// Apply case transformations to repeater items.
$item_schema = isset( $def['item_schema'] ) ? $def['item_schema'] : array();
$items = self::apply_case_to_array_items( $items, $item_schema );
$item_schema = isset($def['item_schema']) ? $def['item_schema'] : array();
$items = self::apply_case_to_array_items($items, $item_schema);

// Use block name for MergeBlock, with alias for legacy behavior.
$fields[ $tbs_name ] = $items;
if ( $alias_key !== $tbs_name ) {
$fields[ $alias_key ] = $items;
$fields[$tbs_name] = $items;
if ($alias_key !== $tbs_name) {
$fields[$alias_key] = $items;
}
self::remember_rich_values_from_array_items( $items );
self::remember_rich_values_from_array_items($items);
continue;
}

$value = self::get_structured_field_value( $structured, $slug, $post_id );
$value = self::get_structured_field_value($structured, $slug, $post_id);
// Force rich type if value contains block HTML, BEFORE prepare strips tags.
$original_type = $type;
if ( ! in_array( $type, array( 'rich', 'html' ), true ) && Documents_Meta_Handler::value_contains_block_html( $value ) ) {
if (!in_array($type, array('rich', 'html'), true) && Documents_Meta_Handler::value_contains_block_html($value)) {
$type = 'rich';
}
$prepared = self::prepare_field_value( $value, $type, $data_type, $def );
$prepared = self::prepare_field_value($value, $type, $data_type, $def);

// Apply case transformation if specified (skip for HTML content).
$field_case = isset( $def['case'] ) ? sanitize_key( $def['case'] ) : '';
if ( '' !== $field_case && ! in_array( $type, array( 'rich', 'html' ), true ) ) {
$prepared = self::apply_case_transformation( $prepared, $field_case );
$field_case = isset($def['case']) ? sanitize_key($def['case']) : '';
if ('' !== $field_case && !in_array($type, array('rich', 'html'), true)) {
$prepared = self::apply_case_transformation($prepared, $field_case);
}

$fields[ $tbs_name ] = $prepared;
if ( $alias_key !== $tbs_name ) {
$fields[ $alias_key ] = $prepared;
$fields[$tbs_name] = $prepared;
if ($alias_key !== $tbs_name) {
$fields[$alias_key] = $prepared;
}
// Register for rich text conversion if typed as rich/html.
// Also check if prepared value still contains HTML as a safety net.
$has_html_in_prepared = Documents_Meta_Handler::value_contains_block_html( $prepared );
if ( in_array( $type, array( 'rich', 'html' ), true ) || $has_html_in_prepared ) {
self::remember_rich_field_value( $prepared );
$has_html_in_prepared = Documents_Meta_Handler::value_contains_block_html($prepared);
if (in_array($type, array('rich', 'html'), true) || $has_html_in_prepared) {
self::remember_rich_field_value($prepared);
}
// Debug logging - enable with WP_DEBUG.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log(
sprintf(
'DOCUMENTATE [%s]: schema_type=%s, effective_type=%s, raw_has_html=%s, prepared_has_html=%s, raw_len=%d, prep_len=%d',
$slug,
$original_type,
$type,
Documents_Meta_Handler::value_contains_block_html( $value ) ? 'YES' : 'NO',
$has_html_in_prepared ? 'YES' : 'NO',
strlen( $value ),
strlen( $prepared )
)
);
if ( Documents_Meta_Handler::value_contains_block_html( $value ) && strlen( $value ) < 500 ) {
error_log( 'DOCUMENTATE [' . $slug . '] RAW: ' . substr( $value, 0, 300 ) );
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf(
'DOCUMENTATE [%s]: schema_type=%s, effective_type=%s, raw_has_html=%s, prepared_has_html=%s, raw_len=%d, prep_len=%d',
$slug,
$original_type,
$type,
Documents_Meta_Handler::value_contains_block_html($value) ? 'YES' : 'NO',
$has_html_in_prepared ? 'YES' : 'NO',
strlen($value),
strlen($prepared),
));
if (Documents_Meta_Handler::value_contains_block_html($value) && strlen($value) < 500) {
error_log('DOCUMENTATE [' . $slug . '] RAW: ' . substr($value, 0, 300));
}
}
}

$logos = get_term_meta( $type_id, 'documentate_type_logos', true );
if ( is_array( $logos ) && ! empty( $logos ) ) {
$i = 1;
foreach ( $logos as $att_id ) {
$att_id = intval( $att_id );
if ( $att_id <= 0 ) {
continue;
$logos = get_term_meta($type_id, 'documentate_type_logos', true);
if (is_array($logos) && !empty($logos)) {
$i = 1;
foreach ($logos as $att_id) {
$att_id = intval($att_id);
if ($att_id <= 0) {
continue;
}
$fields[ 'logo' . $i . '_path' ] = get_attached_file( $att_id );
$fields[ 'logo' . $i . '_url' ] = wp_get_attachment_url( $att_id );
$i++;
$fields['logo' . $i . '_path'] = get_attached_file($att_id);
$fields['logo' . $i . '_url'] = wp_get_attachment_url($att_id);
$i++;
}
}
}

if ( ! empty( $structured ) ) {
foreach ( $structured as $slug => $info ) {
$slug = sanitize_key( $slug );
if ( '' === $slug ) {
continue;
if (!empty($structured)) {
foreach ($structured as $slug => $info) {
$slug = sanitize_key($slug);
if ('' === $slug) {
continue;
}
$placeholder = $slug;
if ( isset( $fields[ $placeholder ] ) && '' !== $fields[ $placeholder ] ) {
continue;
$placeholder = $slug;
if (isset($fields[$placeholder]) && '' !== $fields[$placeholder]) {
continue;
}
if ( isset( $info['type'] ) && 'array' === sanitize_key( $info['type'] ) ) {
$items = self::get_array_field_items_for_merge( $structured, $slug, $post_id );
$fields[ $placeholder ] = $items;
self::remember_rich_values_from_array_items( $items );
continue;
if (isset($info['type']) && 'array' === sanitize_key($info['type'])) {
$items = self::get_array_field_items_for_merge($structured, $slug, $post_id);
$fields[$placeholder] = $items;
self::remember_rich_values_from_array_items($items);
continue;
}

$value = '';
if ( isset( $info['value'] ) ) {
$value = (string) $info['value'];
$value = '';
if (isset($info['value'])) {
$value = (string) $info['value'];
}
if ( '' === $value ) {
$value = self::get_structured_field_value( $structured, $slug, $post_id );
if ('' === $value) {
$value = self::get_structured_field_value($structured, $slug, $post_id);
}
$field_type = isset( $info['type'] ) ? sanitize_key( $info['type'] ) : 'rich';
$field_type = isset($info['type']) ? sanitize_key($info['type']) : 'rich';
// Force rich type if value contains block HTML, BEFORE prepare strips tags.
if ( ! in_array( $field_type, array( 'rich', 'html' ), true ) && Documents_Meta_Handler::value_contains_block_html( $value ) ) {
if (
!in_array($field_type, array('rich', 'html'), true) && Documents_Meta_Handler::value_contains_block_html($value)
) {
$field_type = 'rich';
}
$fields[ $placeholder ] = self::prepare_field_value( $value, $field_type, 'text' );
$fields[$placeholder] = self::prepare_field_value($value, $field_type, 'text');
// Register for rich text conversion if typed as rich/html.
// Also check if prepared value still contains HTML as a safety net.
if ( in_array( $field_type, array( 'rich', 'html' ), true ) || Documents_Meta_Handler::value_contains_block_html( $fields[ $placeholder ] ) ) {
self::remember_rich_field_value( $fields[ $placeholder ] );
if (
in_array($field_type, array('rich', 'html'), true)
|| Documents_Meta_Handler::value_contains_block_html($fields[$placeholder])
) {
self::remember_rich_field_value($fields[$placeholder]);
}
}
}

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method build_merge_fields() has an NPath complexity of 753629339760. The configured NPath complexity threshold is 500.
Comment on lines 411 to -550
@@ -543,23 +534,23 @@ private function build_schema( $placeholders, $template_type, $template_path ) {
* @param array<string,mixed> $token Placeholder token.
* @return array<string,mixed>
*/
private function build_repeater_entry( $token ) {
$name = isset( $token['name'] ) ? (string) $token['name'] : '';
$parameters = isset( $token['parameters'] ) ? $token['parameters'] : array();
private function build_repeater_entry($token) {
$name = isset($token['name']) ? (string) $token['name'] : '';
$parameters = isset($token['parameters']) ? $token['parameters'] : array();

$title = isset( $parameters['title'] ) ? sanitize_text_field( $parameters['title'] ) : '';

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method build_schema() has an NPath complexity of 4212002. The configured NPath complexity threshold is 500.
Comment on lines 584 to +639
@@ -639,35 +630,35 @@ private function build_field_entry( $token ) {
* @param string $text Normalized XML text.
* @return array<int,string>
*/
private function extract_placeholder_chunks( $text ) {
$chunks = array();
$length = strlen( $text );
$in_placeholder = false;
$buffer = '';
$current_quote = null;
$previous_char = '';

for ( $i = 0; $i < $length; $i++ ) {
$char = $text[ $i ];

if ( $in_placeholder ) {
private function extract_placeholder_chunks($text) {
$chunks = array();
$length = strlen($text);
$in_placeholder = false;
$buffer = '';
$current_quote = null;
$previous_char = '';

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method build_field_entry() has a Cyclomatic Complexity of 22. The configured cyclomatic complexity threshold is 15.
Comment on lines 584 to +639
@@ -639,35 +630,35 @@ private function build_field_entry( $token ) {
* @param string $text Normalized XML text.
* @return array<int,string>
*/
private function extract_placeholder_chunks( $text ) {
$chunks = array();
$length = strlen( $text );
$in_placeholder = false;
$buffer = '';
$current_quote = null;
$previous_char = '';

for ( $i = 0; $i < $length; $i++ ) {
$char = $text[ $i ];

if ( $in_placeholder ) {
private function extract_placeholder_chunks($text) {
$chunks = array();
$length = strlen($text);
$in_placeholder = false;
$buffer = '';
$current_quote = null;
$previous_char = '';

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method build_field_entry() has an NPath complexity of 786432. The configured NPath complexity threshold is 500.
Comment on lines 80 to +140
@@ -87,53 +97,57 @@ public function render( WP_Post $post ) {
* @param bool $update Whether this is an existing post being updated.
* @return void
*/
public function save( $post_id, $post = null, $update = false ) {
unset( $update );
public function save($post_id, $post = null, $update = false) {
unset($update);

if ( ! isset( $_POST[ self::NONCE_NAME ] ) ) {
if (!isset($_POST[self::NONCE_NAME])) {
return;
}

$nonce = sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ) );
if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
$nonce = sanitize_text_field(wp_unslash($_POST[self::NONCE_NAME]));
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
return;
}

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}

if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
return;
}

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

if ( null === $post ) {
$post = get_post( $post_id );
if (null === $post) {
$post = get_post($post_id);
}

if ( ! $post instanceof WP_Post ) {
if (!$post instanceof WP_Post) {
return;
}

$title_source = get_post_field( 'post_title', $post_id, 'raw' );
if ( ! is_string( $title_source ) || '' === $title_source ) {
$title_source = get_post_field('post_title', $post_id, 'raw');
if (!is_string($title_source) || '' === $title_source) {
$title_source = $post->post_title;
}

$title_raw = sanitize_text_field( (string) $title_source );
$subject = $this->sanitize_limited_text( $title_raw, 255 );
$author_input = isset( $_POST['documentate_document_meta_author'] ) ? sanitize_text_field( wp_unslash( $_POST['documentate_document_meta_author'] ) ) : '';
$author = $this->sanitize_limited_text( $author_input, 255 );
$keywords_raw = isset( $_POST['documentate_document_meta_keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['documentate_document_meta_keywords'] ) ) : '';
$keywords = $this->sanitize_keywords( $keywords_raw );

$this->persist_meta( $post_id, self::META_KEY_SUBJECT, $subject );
$this->persist_meta( $post_id, self::META_KEY_AUTHOR, $author );
$this->persist_meta( $post_id, self::META_KEY_KEYWORDS, $keywords );
$title_raw = sanitize_text_field((string) $title_source);
$subject = $this->sanitize_limited_text($title_raw, 255);
$author_input = isset($_POST['documentate_document_meta_author'])
? sanitize_text_field(wp_unslash($_POST['documentate_document_meta_author']))

Check warning

Code scanning / PHPMD

Code Size Rules: NPathComplexity Warning

The method save() has an NPath complexity of 3456. The configured NPath complexity threshold is 500.
Comment on lines 340 to 387
* @param string $input_type Input type being rendered.
* @return array<string,string>
*/
public static function build_scalar_input_attributes( $raw_field, $input_type ) {
$attributes = array();
$input_type = sanitize_key( $input_type );
$allows_placeholder = ! in_array( $input_type, array( 'checkbox', 'select' ), true );
public static function build_scalar_input_attributes($raw_field, $input_type) {
$attributes = array();
$input_type = sanitize_key($input_type);
$allows_placeholder = !in_array($input_type, array('checkbox', 'select'), true);

if ( ! is_array( $raw_field ) ) {
if (!is_array($raw_field)) {
return $attributes;
}

if ( $allows_placeholder && ! empty( $raw_field['placeholder'] ) ) {
$attributes['placeholder'] = sanitize_text_field( $raw_field['placeholder'] );
if ($allows_placeholder && !empty($raw_field['placeholder'])) {
$attributes['placeholder'] = sanitize_text_field($raw_field['placeholder']);
}

if ( $allows_placeholder && ! empty( $raw_field['pattern'] ) ) {
if ($allows_placeholder && !empty($raw_field['pattern'])) {
$attributes['pattern'] = (string) $raw_field['pattern'];
}

if ( $allows_placeholder && isset( $raw_field['length'] ) ) {
$length = intval( $raw_field['length'] );
if ( $length > 0 ) {
if ($allows_placeholder && isset($raw_field['length'])) {
$length = intval($raw_field['length']);
if ($length > 0) {
$attributes['maxlength'] = (string) $length;
}
}

$numeric_types = array( 'number', 'range', 'date', 'datetime-local', 'time' );
$numeric_types = array('number', 'range', 'date', 'datetime-local', 'time');

if ( isset( $raw_field['minvalue'] ) && in_array( $input_type, $numeric_types, true ) ) {
if (isset($raw_field['minvalue']) && in_array($input_type, $numeric_types, true)) {
$attributes['min'] = (string) $raw_field['minvalue'];
}

if ( isset( $raw_field['maxvalue'] ) && in_array( $input_type, $numeric_types, true ) ) {
if (isset($raw_field['maxvalue']) && in_array($input_type, $numeric_types, true)) {
$attributes['max'] = (string) $raw_field['maxvalue'];
}

self::add_parameter_attributes( $raw_field, $input_type, $attributes );
self::add_parameter_attributes($raw_field, $input_type, $attributes);

// Add title attribute from pattern message or field title.
if ( ! isset( $attributes['title'] ) ) {
$title_attribute = self::get_field_pattern_message( $raw_field );
if ( '' === $title_attribute ) {
$title_attribute = self::get_field_title( $raw_field );
if (!isset($attributes['title'])) {
$title_attribute = self::get_field_pattern_message($raw_field);
if ('' === $title_attribute) {
$title_attribute = self::get_field_title($raw_field);
}
if ( '' !== $title_attribute ) {
if ('' !== $title_attribute) {
$attributes['title'] = $title_attribute;
}

Check warning

Code scanning / PHPMD

Code Size Rules: CyclomaticComplexity Warning

The method build_scalar_input_attributes() has a Cyclomatic Complexity of 16. The configured cyclomatic complexity threshold is 15.
@erseco erseco merged commit 17c0771 into main Mar 4, 2026
5 checks passed
@erseco erseco deleted the improve-document-flow branch March 4, 2026 11:30
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