mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
feat(sandbox): page the Blocks, Widgets and PHP Snippets tables
All three Sandbox screens rendered every artifact in one table, and each row carries that artifact's source: the snippets table ships every snippet's whole PHP into the DOM, widgets ship compiled PHP, blocks ship block.json plus render.php. With 26 snippets the page carried 26 code blocks; paged, page two carries six. The stores also asked for a flat posts_per_page 200 with no_found_rows, so a site past its 200th artifact lost the rest with nothing on screen saying so. Two bugs a live smoke caught on real data, neither visible to php -l or to a stubbed test: - ORDER BY post_modified is not a total order. 26 seeded snippets shared two timestamps, and across two pages one row appeared twice while another was unreachable: 25 of 26 visible. Fixed with an ID tiebreaker, re-measured at 26 of 26. - The out-of-range clamp was dead code. WP_Query::set_found_posts() bails when a page comes back empty, so such a page reports found_posts = 0 and a clamp testing "total > 0" can never fire in the case it was written for. Confirmed against the database: LIMIT 490,5 on a one-row table returns 0. It now re-queries page 1, which does carry the total, then lands on the last real page. - class-sandbox-list-query.php: one paged query for all three artifact post types, returning a page of IDs plus the real total. - class-admin-pager.php: the windowed pager, lifted out of page-marketplace.php rather than copied three more times. Marketplace now renders through it too, so there is one pager to style. An ellipsis no longer stands in for a single page, which the inherited version could do. - The three stores gain list_*_page(); the flat list_*() are untouched, since the MCP tools want the whole list in one call. - Each view shows "Showing 1-20 of 57" above the table and the pager below. Live-verified on msrplugins.test: page 1 and page 2 render with working links, row actions and code blocks intact, Prev linking back to the clean URL, and the Marketplace unchanged at 36 results across 2 pages. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f5ad66ed42
commit
b7cfe45d58
@@ -4786,3 +4786,43 @@ body[data-emcp-auth="app-password"] [data-authfor="oauth"] { display: none; }
|
||||
@media screen and (max-width: 782px) {
|
||||
.emcp-toasts { right: 12px; bottom: 12px; width: calc(100vw - 24px); }
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Pagination (EMCP_Tools_Admin_Pager)
|
||||
*
|
||||
* Shared by the Marketplace grid and the three Sandbox artifact tables. The
|
||||
* disabled Prev/Next placeholders keep the control the same width at either
|
||||
* end of the run, so it does not shift as you page through.
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
.emcp-pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 24px 0 4px;
|
||||
}
|
||||
|
||||
.emcp-pager__num {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emcp-pager__gap {
|
||||
padding: 0 4px;
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
.emcp-pager .button.disabled {
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/* The "Showing 1-20 of 57" line above a paged table. */
|
||||
.emcp-pager-count {
|
||||
margin: 12px 0 4px;
|
||||
font-size: 12px;
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* The admin pager, shared by every paged list screen.
|
||||
*
|
||||
* This started as sixty lines inline in the Marketplace view. The Sandbox
|
||||
* screens (Blocks / Widgets / PHP Snippets) needed the same control, so the
|
||||
* window maths and the markup moved here rather than being copied three more
|
||||
* times. The Marketplace now renders through this class too, so there is one
|
||||
* pager to style and one place where its behaviour is defined.
|
||||
*
|
||||
* `window()` is pure on purpose: which numbers appear around the current page
|
||||
* is the only part with logic worth pinning in a test.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.15.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Windowed pagination control for admin list screens.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*/
|
||||
class EMCP_Tools_Admin_Pager {
|
||||
|
||||
/**
|
||||
* Page counts up to this many are shown in full, with no ellipsis.
|
||||
*
|
||||
* @since 3.15.0
|
||||
* @var int
|
||||
*/
|
||||
const FULL_RUN = 7;
|
||||
|
||||
/**
|
||||
* Marks an ellipsis in a `window()` result. Zero is never a page number,
|
||||
* so it cannot collide with one.
|
||||
*
|
||||
* @since 3.15.0
|
||||
* @var int
|
||||
*/
|
||||
const GAP = 0;
|
||||
|
||||
/**
|
||||
* Reads the requested page number from the query string.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $key Query argument holding the page number.
|
||||
* @return int 1 or higher.
|
||||
*/
|
||||
public static function current( string $key = 'paged' ): int {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination, no state change.
|
||||
$raw = isset( $_GET[ $key ] ) ? wp_unslash( $_GET[ $key ] ) : '';
|
||||
return max( 1, absint( is_scalar( $raw ) ? $raw : 0 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* The page numbers to show, with self::GAP standing in for an ellipsis.
|
||||
*
|
||||
* Short runs are listed in full. Longer ones keep the first page, the last
|
||||
* page, and the current page with a neighbour either side, so the control
|
||||
* stays a fixed width however many pages there are.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param int $current Current page (1-based).
|
||||
* @param int $pages Total pages.
|
||||
* @return int[] Page numbers, with self::GAP for each ellipsis.
|
||||
*/
|
||||
public static function window( int $current, int $pages ): array {
|
||||
$pages = max( 1, $pages );
|
||||
$current = min( max( 1, $current ), $pages );
|
||||
|
||||
if ( $pages <= self::FULL_RUN ) {
|
||||
return range( 1, $pages );
|
||||
}
|
||||
|
||||
// The pages worth keeping: both ends, and the current one with a
|
||||
// neighbour either side.
|
||||
$keep = array( 1, $pages );
|
||||
for ( $n = $current - 1; $n <= $current + 1; $n++ ) {
|
||||
if ( $n >= 1 && $n <= $pages ) {
|
||||
$keep[] = $n;
|
||||
}
|
||||
}
|
||||
$keep = array_unique( $keep );
|
||||
sort( $keep );
|
||||
|
||||
// Close the holes between them. A hole of exactly one page becomes that
|
||||
// page: an ellipsis is wider than the single number it would hide.
|
||||
$out = array();
|
||||
foreach ( $keep as $i => $n ) {
|
||||
if ( $i > 0 ) {
|
||||
$hole = $n - $keep[ $i - 1 ];
|
||||
if ( 2 === $hole ) {
|
||||
$out[] = $n - 1;
|
||||
} elseif ( $hole > 2 ) {
|
||||
$out[] = self::GAP;
|
||||
}
|
||||
}
|
||||
$out[] = $n;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the pager, or an empty string when there is only one page.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param int $current Current page (1-based).
|
||||
* @param int $pages Total pages.
|
||||
* @param callable $href Given a page number, returns its URL.
|
||||
* @return string Escaped HTML.
|
||||
*/
|
||||
public static function render( int $current, int $pages, callable $href ): string {
|
||||
$pages = max( 1, $pages );
|
||||
if ( $pages < 2 ) {
|
||||
return '';
|
||||
}
|
||||
$current = min( max( 1, $current ), $pages );
|
||||
|
||||
$out = '<nav class="emcp-pager" aria-label="' . esc_attr__( 'Pagination', 'emcp-tools' ) . '">';
|
||||
$out .= self::edge( $current > 1, $href, $current - 1, '← ' . esc_html__( 'Prev', 'emcp-tools' ) );
|
||||
|
||||
foreach ( self::window( $current, $pages ) as $n ) {
|
||||
if ( self::GAP === $n ) {
|
||||
$out .= '<span class="emcp-pager__gap" aria-hidden="true">…</span>';
|
||||
} elseif ( $n === $current ) {
|
||||
$out .= '<span class="button button-primary emcp-pager__num" aria-current="page">' . esc_html( (string) $n ) . '</span>';
|
||||
} else {
|
||||
$out .= '<a class="button emcp-pager__num" href="' . esc_url( (string) $href( $n ) ) . '">' . esc_html( (string) $n ) . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
$out .= self::edge( $current < $pages, $href, $current + 1, esc_html__( 'Next', 'emcp-tools' ) . ' →' );
|
||||
$out .= '</nav>';
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Prev/Next control: a link when there is somewhere to go, otherwise a
|
||||
* disabled placeholder so the control does not change width at the ends.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param bool $enabled Whether the target page exists.
|
||||
* @param callable $href Given a page number, returns its URL.
|
||||
* @param int $target Page to link to.
|
||||
* @param string $label Pre-escaped label.
|
||||
* @return string
|
||||
*/
|
||||
private static function edge( bool $enabled, callable $href, int $target, string $label ): string {
|
||||
if ( ! $enabled ) {
|
||||
return '<span class="button disabled emcp-pager__edge">' . $label . '</span>';
|
||||
}
|
||||
return '<a class="button emcp-pager__edge" href="' . esc_url( (string) $href( $target ) ) . '">' . $label . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Showing 1-20 of 57" line that sits above a paged table.
|
||||
*
|
||||
* Returns a bare count when everything fits on one page: a range reading
|
||||
* "1-4 of 4" is noise.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param int $page Current page (1-based).
|
||||
* @param int $per_page Rows per page.
|
||||
* @param int $shown Rows on this page.
|
||||
* @param int $total Rows in total.
|
||||
* @return string Escaped text.
|
||||
*/
|
||||
public static function summary( int $page, int $per_page, int $shown, int $total ): string {
|
||||
if ( $total <= 0 ) {
|
||||
return '';
|
||||
}
|
||||
if ( $shown >= $total ) {
|
||||
/* translators: %d: number of items. */
|
||||
return esc_html( sprintf( _n( '%d item', '%d items', $total, 'emcp-tools' ), $total ) );
|
||||
}
|
||||
|
||||
$first = ( max( 1, $page ) - 1 ) * max( 1, $per_page ) + 1;
|
||||
|
||||
return esc_html(
|
||||
sprintf(
|
||||
/* translators: 1: first item number, 2: last item number, 3: total items. */
|
||||
__( 'Showing %1$d-%2$d of %3$d', 'emcp-tools' ),
|
||||
$first,
|
||||
$first + $shown - 1,
|
||||
$total
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -239,60 +239,12 @@ $emcp_per_page = 24;
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( $emcp_pages > 1 ) : ?>
|
||||
<nav class="emcp-mk-pager" aria-label="<?php esc_attr_e( 'Pagination', 'emcp-tools' ); ?>">
|
||||
<?php if ( $emcp_cur_page > 1 ) : ?>
|
||||
<a class="button emcp-mk-pager__edge" href="<?php echo esc_url( $emcp_page_href( $emcp_cur_page - 1 ) ); ?>">← <?php esc_html_e( 'Prev', 'emcp-tools' ); ?></a>
|
||||
<?php else : ?>
|
||||
<span class="button disabled emcp-mk-pager__edge">← <?php esc_html_e( 'Prev', 'emcp-tools' ); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Windowed page numbers: first, last, current ±1, ellipses.
|
||||
$emcp_nums = array();
|
||||
if ( $emcp_pages <= 7 ) {
|
||||
for ( $i = 1; $i <= $emcp_pages; $i++ ) {
|
||||
$emcp_nums[] = $i;
|
||||
}
|
||||
} else {
|
||||
$emcp_nums[] = 1;
|
||||
$emcp_lo = max( 2, $emcp_cur_page - 1 );
|
||||
$emcp_hi = min( $emcp_pages - 1, $emcp_cur_page + 1 );
|
||||
if ( $emcp_lo > 2 ) {
|
||||
$emcp_nums[] = 0;
|
||||
}
|
||||
for ( $i = $emcp_lo; $i <= $emcp_hi; $i++ ) {
|
||||
$emcp_nums[] = $i;
|
||||
}
|
||||
if ( $emcp_hi < $emcp_pages - 1 ) {
|
||||
$emcp_nums[] = 0;
|
||||
}
|
||||
$emcp_nums[] = $emcp_pages;
|
||||
}
|
||||
foreach ( $emcp_nums as $emcp_n ) :
|
||||
if ( 0 === $emcp_n ) :
|
||||
?>
|
||||
<span class="emcp-mk-pager__gap" aria-hidden="true">…</span>
|
||||
<?php
|
||||
elseif ( $emcp_n === $emcp_cur_page ) :
|
||||
?>
|
||||
<span class="button button-primary emcp-mk-pager__num" aria-current="page"><?php echo esc_html( (string) $emcp_n ); ?></span>
|
||||
<?php
|
||||
else :
|
||||
?>
|
||||
<a class="button emcp-mk-pager__num" href="<?php echo esc_url( $emcp_page_href( $emcp_n ) ); ?>"><?php echo esc_html( (string) $emcp_n ); ?></a>
|
||||
<?php
|
||||
endif;
|
||||
endforeach;
|
||||
?>
|
||||
|
||||
<?php if ( $emcp_cur_page < $emcp_pages ) : ?>
|
||||
<a class="button emcp-mk-pager__edge" href="<?php echo esc_url( $emcp_page_href( $emcp_cur_page + 1 ) ); ?>"><?php esc_html_e( 'Next', 'emcp-tools' ); ?> →</a>
|
||||
<?php else : ?>
|
||||
<span class="button disabled emcp-mk-pager__edge"><?php esc_html_e( 'Next', 'emcp-tools' ); ?> →</span>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// The pager is shared with the Sandbox screens, so the window maths and
|
||||
// markup live in EMCP_Tools_Admin_Pager rather than in this view.
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by render().
|
||||
echo EMCP_Tools_Admin_Pager::render( $emcp_cur_page, $emcp_pages, $emcp_page_href );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -321,8 +273,4 @@ $emcp_per_page = 24;
|
||||
.emcp-mk-card__ver { flex: none; width: 15px; height: 15px; display: block; }
|
||||
.emcp-mk-card__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12px; color: #787c82; margin-top: 4px; }
|
||||
.emcp-mk-card__cta { margin-top: 8px; }
|
||||
.emcp-mk-pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; margin: 24px 0 4px; }
|
||||
.emcp-mk-pager__num { min-width: 34px; text-align: center; justify-content: center; }
|
||||
.emcp-mk-pager__gap { padding: 0 4px; color: #787c82; }
|
||||
.emcp-mk-pager .button.disabled { pointer-events: none; opacity: .5; }
|
||||
</style>
|
||||
|
||||
@@ -68,7 +68,24 @@ $emcp_tools_bb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<?php $emcp_tools_bb_list = EMCP_Tools_Block_Store::instance()->list_blocks( 'any' ); ?>
|
||||
<?php
|
||||
// A page at a time. Every row below carries that block's block.json and
|
||||
// render.php, so an unpaged table grows with the site, not the screen.
|
||||
$emcp_tools_bb_pg = EMCP_Tools_Block_Store::instance()->list_blocks_page( 'any', EMCP_Tools_Admin_Pager::current() );
|
||||
$emcp_tools_bb_list = $emcp_tools_bb_pg['items'];
|
||||
|
||||
// `paged`, not `page`: wp-admin already uses that one for the menu slug.
|
||||
$emcp_tools_bb_href = static function ( $emcp_n ) {
|
||||
$args = array(
|
||||
'page' => 'emcp-tools-widgets',
|
||||
'view' => 'blocks',
|
||||
);
|
||||
if ( $emcp_n > 1 ) {
|
||||
$args['paged'] = (int) $emcp_n;
|
||||
}
|
||||
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="notice notice-warning inline" style="margin: 12px 0;">
|
||||
<p>
|
||||
@@ -100,6 +117,9 @@ $emcp_tools_bb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<p class="description emcp-pager-count">
|
||||
<?php echo EMCP_Tools_Admin_Pager::summary( $emcp_tools_bb_pg['page'], $emcp_tools_bb_pg['per_page'], count( $emcp_tools_bb_list ), $emcp_tools_bb_pg['total'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by summary(). ?>
|
||||
</p>
|
||||
<table class="widefat striped elementor-mcp-blocks-table" data-nonce="<?php echo esc_attr( wp_create_nonce( 'emcp_tools_blocks' ) ); ?>" style="margin-top: 16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -167,6 +187,11 @@ $emcp_tools_bb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by render().
|
||||
echo EMCP_Tools_Admin_Pager::render( $emcp_tools_bb_pg['page'], $emcp_tools_bb_pg['pages'], $emcp_tools_bb_href );
|
||||
?>
|
||||
|
||||
<script>
|
||||
( function () {
|
||||
var table = document.querySelector( '.elementor-mcp-blocks-table' );
|
||||
|
||||
@@ -41,8 +41,35 @@ $emcp_tools_sn_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
<?php
|
||||
// ===== PHP Snippets (free, capability-gated) =====
|
||||
$emcp_tools_sn_can = class_exists( 'EMCP_Tools_PHP_Snippet_Store' ) && EMCP_Tools_PHP_Snippet_Store::can_edit();
|
||||
$emcp_tools_sn_list = class_exists( 'EMCP_Tools_PHP_Snippet_Store' ) ? EMCP_Tools_PHP_Snippet_Store::list_snippets( 'any' ) : array();
|
||||
$emcp_tools_sn_nonce = wp_create_nonce( 'emcp_tools_php_snippets' );
|
||||
|
||||
// A page at a time. Every row below carries that snippet's whole source, so an
|
||||
// unpaged table grows with the site rather than with the screen.
|
||||
$emcp_tools_sn_pg = class_exists( 'EMCP_Tools_PHP_Snippet_Store' )
|
||||
? EMCP_Tools_PHP_Snippet_Store::list_snippets_page( 'any', EMCP_Tools_Admin_Pager::current() )
|
||||
: array(
|
||||
'items' => array(),
|
||||
'total' => 0,
|
||||
'page' => 1,
|
||||
'pages' => 1,
|
||||
'per_page' => EMCP_Tools_Sandbox_List_Query::PER_PAGE,
|
||||
);
|
||||
$emcp_tools_sn_list = $emcp_tools_sn_pg['items'];
|
||||
|
||||
/**
|
||||
* Links a page number back to this view, keeping the Sandbox route intact.
|
||||
* `paged`, not `page`: wp-admin already uses that one for the menu slug.
|
||||
*/
|
||||
$emcp_tools_sn_href = static function ( $emcp_n ) {
|
||||
$args = array(
|
||||
'page' => 'emcp-tools-widgets',
|
||||
'view' => 'snippets',
|
||||
);
|
||||
if ( $emcp_n > 1 ) {
|
||||
$args['paged'] = (int) $emcp_n;
|
||||
}
|
||||
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
||||
};
|
||||
?>
|
||||
<div class="elementor-mcp-pro-prompts elementor-mcp-php-snippets" data-nonce="<?php echo esc_attr( $emcp_tools_sn_nonce ); ?>" style="margin-top: 28px;">
|
||||
<div class="elementor-mcp-pro-prompts-header">
|
||||
@@ -127,6 +154,9 @@ $emcp_tools_sn_nonce = wp_create_nonce( 'emcp_tools_php_snippets' );
|
||||
<?php if ( empty( $emcp_tools_sn_list ) ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'No snippets yet. Add one above, or ask your AI agent to draft one with the create-php-snippet tool.', 'emcp-tools' ); ?></p>
|
||||
<?php else : ?>
|
||||
<p class="description emcp-pager-count">
|
||||
<?php echo EMCP_Tools_Admin_Pager::summary( $emcp_tools_sn_pg['page'], $emcp_tools_sn_pg['per_page'], count( $emcp_tools_sn_list ), $emcp_tools_sn_pg['total'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by summary(). ?>
|
||||
</p>
|
||||
<table class="widefat striped elementor-mcp-snippets-table" style="margin-top: 8px;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -230,6 +260,11 @@ $emcp_tools_sn_nonce = wp_create_nonce( 'emcp_tools_php_snippets' );
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by render().
|
||||
echo EMCP_Tools_Admin_Pager::render( $emcp_tools_sn_pg['page'], $emcp_tools_sn_pg['pages'], $emcp_tools_sn_href );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -70,7 +70,24 @@ $emcp_tools_wb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<?php $emcp_tools_wb_list = EMCP_Tools_Widget_Store::list_widgets( 'any' ); ?>
|
||||
<?php
|
||||
// A page at a time. Every row below carries that widget's compiled PHP,
|
||||
// so an unpaged table grows with the site rather than with the screen.
|
||||
$emcp_tools_wb_pg = EMCP_Tools_Widget_Store::list_widgets_page( 'any', EMCP_Tools_Admin_Pager::current() );
|
||||
$emcp_tools_wb_list = $emcp_tools_wb_pg['items'];
|
||||
|
||||
// `paged`, not `page`: wp-admin already uses that one for the menu slug.
|
||||
$emcp_tools_wb_href = static function ( $emcp_n ) {
|
||||
$args = array(
|
||||
'page' => 'emcp-tools-widgets',
|
||||
'view' => 'widgets',
|
||||
);
|
||||
if ( $emcp_n > 1 ) {
|
||||
$args['paged'] = (int) $emcp_n;
|
||||
}
|
||||
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="notice notice-warning inline" style="margin: 12px 0;">
|
||||
<p>
|
||||
@@ -102,6 +119,9 @@ $emcp_tools_wb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<p class="description emcp-pager-count">
|
||||
<?php echo EMCP_Tools_Admin_Pager::summary( $emcp_tools_wb_pg['page'], $emcp_tools_wb_pg['per_page'], count( $emcp_tools_wb_list ), $emcp_tools_wb_pg['total'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by summary(). ?>
|
||||
</p>
|
||||
<table class="widefat striped elementor-mcp-widgets-table" data-nonce="<?php echo esc_attr( wp_create_nonce( 'emcp_tools_widgets' ) ); ?>" style="margin-top: 16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -168,6 +188,11 @@ $emcp_tools_wb_import_error = isset( $_GET['import_error'] ) ? sanitize_text_fie
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by render().
|
||||
echo EMCP_Tools_Admin_Pager::render( $emcp_tools_wb_pg['page'], $emcp_tools_wb_pg['pages'], $emcp_tools_wb_href );
|
||||
?>
|
||||
|
||||
<script>
|
||||
( function () {
|
||||
var table = document.querySelector( '.elementor-mcp-widgets-table' );
|
||||
|
||||
@@ -207,6 +207,9 @@ class EMCP_Tools_Bootstrap {
|
||||
// Central sandbox storage location (wp-content/emcp-sandbox). Every store
|
||||
// resolves paths through this, so it must load before them.
|
||||
require_once EMCP_TOOLS_DIR . 'includes/sandbox/class-sandbox-paths.php';
|
||||
// Paged listing shared by all three artifact stores. Loaded alongside
|
||||
// them, not with the admin, because the stores name it in a default.
|
||||
require_once EMCP_TOOLS_DIR . 'includes/sandbox/class-sandbox-list-query.php';
|
||||
require_once EMCP_TOOLS_DIR . 'includes/class-widget-store.php';
|
||||
require_once EMCP_TOOLS_DIR . 'includes/class-widget-loader.php';
|
||||
// Sandbox Bundle — portable cloud-ready format for blocks/widgets/snippets.
|
||||
@@ -501,6 +504,7 @@ class EMCP_Tools_Bootstrap {
|
||||
* @since 2.1.0
|
||||
*/
|
||||
private static function load_admin(): void {
|
||||
require_once EMCP_TOOLS_DIR . 'includes/admin/class-admin-pager.php';
|
||||
require_once EMCP_TOOLS_DIR . 'includes/admin/class-admin.php';
|
||||
require_once EMCP_TOOLS_DIR . 'includes/admin/class-mcpb-builder.php';
|
||||
|
||||
|
||||
@@ -512,6 +512,40 @@ class EMCP_Tools_PHP_Snippet_Store {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* One page of snippets, plus the totals needed to draw a pager.
|
||||
*
|
||||
* list_snippets() stays as it is for the MCP tools, which want the whole
|
||||
* list in a single call. The admin table renders each row's full source,
|
||||
* so it takes a page at a time instead.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $status 'active', 'draft', or 'any'.
|
||||
* @param int $page 1-based page number.
|
||||
* @param int $per_page Rows per page.
|
||||
* @return array{items:array[],total:int,page:int,pages:int,per_page:int}
|
||||
*/
|
||||
public static function list_snippets_page( string $status = 'any', int $page = 1, int $per_page = EMCP_Tools_Sandbox_List_Query::PER_PAGE ): array {
|
||||
$result = EMCP_Tools_Sandbox_List_Query::page( self::POST_TYPE, $status, $page, $per_page );
|
||||
|
||||
$items = array();
|
||||
foreach ( $result['ids'] as $emcp_id ) {
|
||||
$summary = self::summary( $emcp_id );
|
||||
if ( ! is_wp_error( $summary ) ) {
|
||||
$items[] = $summary;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'items' => $items,
|
||||
'total' => $result['total'],
|
||||
'page' => $result['page'],
|
||||
'pages' => $result['pages'],
|
||||
'per_page' => $result['per_page'],
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Manifest
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -723,6 +723,40 @@ class EMCP_Tools_Widget_Store {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* One page of widgets, plus the totals needed to draw a pager.
|
||||
*
|
||||
* list_widgets() stays as it is for the MCP tools, which want the whole
|
||||
* list in a single call. The admin table renders each row's compiled PHP,
|
||||
* so it takes a page at a time instead.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $status 'active', 'draft', or 'any'.
|
||||
* @param int $page 1-based page number.
|
||||
* @param int $per_page Rows per page.
|
||||
* @return array{items:array[],total:int,page:int,pages:int,per_page:int}
|
||||
*/
|
||||
public static function list_widgets_page( string $status = 'any', int $page = 1, int $per_page = EMCP_Tools_Sandbox_List_Query::PER_PAGE ): array {
|
||||
$result = EMCP_Tools_Sandbox_List_Query::page( self::POST_TYPE, $status, $page, $per_page );
|
||||
|
||||
$items = array();
|
||||
foreach ( $result['ids'] as $emcp_id ) {
|
||||
$summary = self::summary( $emcp_id );
|
||||
if ( ! is_wp_error( $summary ) ) {
|
||||
$items[] = $summary;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'items' => $items,
|
||||
'total' => $result['total'],
|
||||
'page' => $result['page'],
|
||||
'pages' => $result['pages'],
|
||||
'per_page' => $result['per_page'],
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Manifest
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* One paged query behind every sandbox artifact list.
|
||||
*
|
||||
* The three sandbox stores (blocks, widgets, PHP snippets) are the same shape:
|
||||
* a private CPT, ordered by modified date, filtered by status. All three asked
|
||||
* for a flat 200 rows with `no_found_rows`, which had two consequences. The
|
||||
* admin screens rendered every artifact in a single table (and each row of the
|
||||
* snippet and widget tables carries that artifact's whole source), and a site
|
||||
* past its 200th artifact silently lost the rest with nothing on screen saying
|
||||
* so.
|
||||
*
|
||||
* Owning the query here fixes both once: callers get one page of IDs plus the
|
||||
* real total, and each store maps those IDs through its own summary shape.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.15.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paged listing for the sandbox artifact post types.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*/
|
||||
class EMCP_Tools_Sandbox_List_Query {
|
||||
|
||||
/**
|
||||
* Rows per page on the sandbox management screens.
|
||||
*
|
||||
* @since 3.15.0
|
||||
* @var int
|
||||
*/
|
||||
const PER_PAGE = 20;
|
||||
|
||||
/**
|
||||
* Upper bound on `per_page`, so a hand-edited URL cannot ask for the whole
|
||||
* table back and undo the point of paging.
|
||||
*
|
||||
* @since 3.15.0
|
||||
* @var int
|
||||
*/
|
||||
const MAX_PER_PAGE = 100;
|
||||
|
||||
/**
|
||||
* Returns one page of artifact IDs plus the totals needed to draw a pager.
|
||||
*
|
||||
* A requested page beyond the end lands on the last page that exists rather
|
||||
* than on an empty table: that is what a bookmarked URL looks like once rows
|
||||
* have been deleted, and an empty screen reads as data loss.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $post_type Artifact post type.
|
||||
* @param string $status 'active', 'draft', or 'any'.
|
||||
* @param int $page 1-based page number.
|
||||
* @param int $per_page Rows per page.
|
||||
* @return array{ids:int[],total:int,page:int,pages:int,per_page:int}
|
||||
*/
|
||||
public static function page( string $post_type, string $status = 'any', int $page = 1, int $per_page = self::PER_PAGE ): array {
|
||||
$per_page = max( 1, min( self::MAX_PER_PAGE, $per_page ) );
|
||||
$page = max( 1, $page );
|
||||
$post_status = self::post_status( $status );
|
||||
|
||||
$result = self::query( $post_type, $post_status, $page, $per_page );
|
||||
|
||||
// WP_Query::set_found_posts() bails when the page came back empty, so an
|
||||
// out-of-range page reports a total of zero and cannot say where the list
|
||||
// really ends. Page 1 does carry the total, so ask it, then land on the
|
||||
// last page that exists.
|
||||
if ( empty( $result['ids'] ) && $page > 1 ) {
|
||||
$page = 1;
|
||||
$result = self::query( $post_type, $post_status, 1, $per_page );
|
||||
|
||||
if ( $result['pages'] > 1 ) {
|
||||
$page = $result['pages'];
|
||||
$result = self::query( $post_type, $post_status, $page, $per_page );
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'ids' => $result['ids'],
|
||||
'total' => $result['total'],
|
||||
'page' => $page,
|
||||
'pages' => $result['pages'],
|
||||
'per_page' => $per_page,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the stores' status vocabulary onto post statuses.
|
||||
*
|
||||
* Drafts are the whole point of the sandbox (AI writes them, a human
|
||||
* activates them), so 'any' has to mean both.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $status 'active', 'draft', or anything else for both.
|
||||
* @return string
|
||||
*/
|
||||
public static function post_status( string $status ): string {
|
||||
if ( 'active' === $status ) {
|
||||
return 'publish';
|
||||
}
|
||||
if ( 'draft' === $status ) {
|
||||
return 'draft';
|
||||
}
|
||||
return 'any';
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the query for one page.
|
||||
*
|
||||
* @since 3.15.0
|
||||
*
|
||||
* @param string $post_type Artifact post type.
|
||||
* @param string $post_status Resolved post status.
|
||||
* @param int $page 1-based page number.
|
||||
* @param int $per_page Rows per page.
|
||||
* @return array{ids:int[],total:int,pages:int}
|
||||
*/
|
||||
private static function query( string $post_type, string $post_status, int $page, int $per_page ): array {
|
||||
$query = new WP_Query(
|
||||
array(
|
||||
'post_type' => $post_type,
|
||||
'post_status' => $post_status,
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $page,
|
||||
// Modified date alone is not a total order: artifacts saved in the
|
||||
// same second tie, and MySQL may order the tie differently for
|
||||
// `LIMIT 0,20` than for `LIMIT 20,20`. That shows one row on two
|
||||
// pages and hides another entirely. ID is unique, so it settles it.
|
||||
'orderby' => array(
|
||||
'modified' => 'DESC',
|
||||
'ID' => 'DESC',
|
||||
),
|
||||
'fields' => 'ids',
|
||||
// The whole point is the total, so found_rows stays on here.
|
||||
'no_found_rows' => false,
|
||||
)
|
||||
);
|
||||
|
||||
$ids = array();
|
||||
foreach ( (array) $query->posts as $post ) {
|
||||
// 'fields' => 'ids' gives plain integers, but a filtered query (or a
|
||||
// test double) can still hand back objects.
|
||||
$ids[] = is_object( $post ) ? (int) $post->ID : (int) $post;
|
||||
}
|
||||
|
||||
$total = (int) $query->found_posts;
|
||||
|
||||
return array(
|
||||
'ids' => $ids,
|
||||
'total' => $total,
|
||||
'pages' => max( 1, (int) ceil( $total / $per_page ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule pro updated: 9df5f6572c...f0f4351eac
Reference in New Issue
Block a user