PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.2.13
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.2.13
2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 trunk 1.0.0 1.0.1 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2
table-builder-block / includes / Config / CPT / TableCPTAdmin.php

TableCPTAdmin.php in TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables 2.2.13, at includes/Config/CPT/TableCPTAdmin.php

523 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin list-table integration for the "tablekit_table" post type
4 *
5 * @package TableKit
6 */
7
8 namespace TableBuilder\Config\CPT;
9
10 defined( 'ABSPATH' ) || exit;
11
12 use WP_Post;
13
14 /**
15 * Adds source/shortcode columns, row actions, and notices to the Tables admin list table.
16 */
17 class TableCPTAdmin {
18
19 /**
20 * Scans post content for inline table blocks.
21 *
22 * @var InlineTableScanner
23 */
24 private InlineTableScanner $scanner;
25
26 /**
27 * Builds virtual "tablekit_table" posts for inline-only tables.
28 *
29 * @var VirtualPostBuilder
30 */
31 private VirtualPostBuilder $virtual_posts;
32
33 /**
34 * Stores the collaborators used to resolve and render inline-table admin data.
35 *
36 * @param InlineTableScanner $scanner Scanner used to locate inline table blocks.
37 * @param VirtualPostBuilder $virtual_posts Builder used to render shortcode inputs/panels for admin list columns.
38 */
39 public function __construct( InlineTableScanner $scanner, VirtualPostBuilder $virtual_posts ) {
40 $this->scanner = $scanner;
41 $this->virtual_posts = $virtual_posts;
42 }
43
44 /**
45 * Prints an admin notice on the Tables list screen recommending a single
46 * table per page for easier management/performance.
47 *
48 * @return void
49 */
50 public function print_recommendation_notice(): void {
51 if ( ! is_admin() ) {
52 return;
53 }
54
55 $screen = get_current_screen();
56 if ( ! $screen || TableCPT::POST_TYPE !== $screen->post_type ) {
57 return;
58 }
59
60 $message = __( 'TableKit supports multiple tables in the same page. However, to keep your content easier to manage and improve compatibility, performance, and shortcode handling, we recommend creating a single table whenever possible.', 'table-builder-block' );
61
62 echo '<div class="notice notice-info is-dismissible" >';
63 echo '<p>' . esc_html( $message ) . '</p>';
64 echo '</div>';
65 }
66
67 /**
68 * Remap any virtual (negative, synthetic) post IDs in a bulk action's
69 * selected posts back to their real source post IDs, before WP core's own
70 * bulk-action handling runs on edit.php. Hooked to "load-edit.php".
71 *
72 * @return void
73 */
74 public function prepare_bulk_post_ids(): void {
75 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- this only
76 // remaps virtual->real post IDs ahead of WP core's own bulk-action handling on
77 // edit.php (load-edit.php runs before edit.php verifies the bulk-action nonce);
78 // it never performs a state change itself.
79 $post_type = isset( $_REQUEST['post_type'] ) ? sanitize_key( wp_unslash( $_REQUEST['post_type'] ) ) : '';
80
81 if ( '' === $post_type || TableCPT::POST_TYPE !== $post_type ) {
82 return;
83 }
84
85 if ( empty( $_REQUEST['post'] ) ) {
86 return;
87 }
88
89 $virtual_source_map = $this->virtual_posts->get_virtual_source_id_map();
90
91 if ( empty( $virtual_source_map ) ) {
92 return;
93 }
94
95 $post_ids = array_map( 'intval', (array) wp_unslash( $_REQUEST['post'] ) );
96 // phpcs:enable WordPress.Security.NonceVerification.Recommended
97 $normalized_ids = array();
98
99 foreach ( $post_ids as $post_id ) {
100 if ( $post_id > 0 ) {
101 $normalized_ids[] = $post_id;
102 continue;
103 }
104
105 if ( isset( $virtual_source_map[ $post_id ] ) ) {
106 $normalized_ids[] = (int) $virtual_source_map[ $post_id ];
107 }
108 }
109
110 $normalized_ids = array_values( array_unique( array_filter( $normalized_ids ) ) );
111
112 $_REQUEST['post'] = $normalized_ids;
113 $_POST['post'] = $normalized_ids;
114 }
115
116 /**
117 * Determines whether a bulk-action checkbox should show for a list-table row,
118 * gating virtual rows on the viewer's capability over the real source post.
119 *
120 * @param bool $show Default visibility as determined by WP core.
121 * @param WP_Post $post The row's post (real or virtual).
122 * @return bool
123 */
124 public function show_list_table_checkbox( bool $show, WP_Post $post ): bool {
125 if ( empty( $post->tablekit_is_virtual ) ) {
126 return $show;
127 }
128
129 return current_user_can( 'edit_post', (int) $post->tablekit_source_id );
130 }
131
132 /**
133 * Adds TableKit-specific columns (source/author, and shortcode columns when
134 * Pro is active) to the Tables admin list table, dropping the default "author" column.
135 *
136 * @param array $columns Existing list table columns.
137 * @return array Modified columns.
138 */
139 public function add_list_columns( array $columns ): array {
140 $updated = array();
141
142 foreach ( $columns as $key => $label ) {
143 if ( 'author' === $key ) {
144 continue;
145 }
146
147 $updated[ $key ] = $label;
148
149 if ( 'title' === $key ) {
150 if ( $this->is_pro_active() ) {
151 $updated['tablekit_shortcode'] = __( 'Shortcode', 'table-builder-block' );
152 $updated['tablekit_child_shortcode'] = __( 'Child Shortcodes', 'table-builder-block' );
153 }
154
155 $updated['tablekit_source'] = __( 'Source', 'table-builder-block' );
156 $updated['tablekit_author'] = __( 'Author', 'table-builder-block' );
157 }
158 }
159
160 return $updated;
161 }
162
163 /**
164 * Renders the contents of a custom Tables admin list-table column.
165 *
166 * @param string $column Column key being rendered.
167 * @param int $post_id The row's post ID (unused directly; the global $post is used instead).
168 * @return void
169 */
170 public function render_list_column( string $column, int $post_id ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- required by the "manage_{$post_type}_posts_custom_column" action signature; the global $post is used instead.
171 global $post;
172
173 if ( ! ( $post instanceof WP_Post ) ) {
174 return;
175 }
176
177 switch ( $column ) {
178 case 'tablekit_shortcode':
179 if ( ! $this->is_pro_active() ) {
180 break;
181 }
182
183 $this->render_column_shortcode( $post );
184 break;
185 case 'tablekit_child_shortcode':
186 if ( ! $this->is_pro_active() ) {
187 break;
188 }
189
190 $this->render_column_child_shortcodes( $post );
191 break;
192 case 'tablekit_source':
193 $this->render_column_source( $post );
194 break;
195 case 'tablekit_author':
196 $this->render_column_author( $post );
197 break;
198 }
199 }
200
201 /**
202 * Replace a virtual row's default row actions (Edit/Trash/Delete, etc.) with
203 * actions pointed at its real source post instead of the non-existent virtual post.
204 *
205 * @param array $actions Default row actions.
206 * @param WP_Post $post The row's post (real or virtual).
207 * @return array Row actions to display.
208 */
209 public function filter_row_actions( array $actions, WP_Post $post ): array {
210 if ( empty( $post->tablekit_is_virtual ) ) {
211 return $actions;
212 }
213
214 $source_id = (int) $post->tablekit_source_id;
215 $source_post = get_post( $source_id );
216 $source_actions = array();
217 $title = (string) $post->tablekit_source_title;
218
219 if ( $source_post instanceof WP_Post && current_user_can( 'edit_post', $source_id ) && 'trash' !== $source_post->post_status ) {
220 $edit_link = get_edit_post_link( $source_id );
221
222 if ( false !== $edit_link && '' !== $edit_link ) {
223 $source_actions['edit'] = sprintf(
224 '<a href="%s" aria-label="%s">%s</a>',
225 esc_url( (string) $edit_link ),
226 /* translators: %s: Title of the source post the table belongs to. */
227 esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'table-builder-block' ), $title ) ),
228 esc_html__( 'Edit', 'table-builder-block' )
229 );
230 }
231
232 if ( 'publish' === $source_post->post_status ) {
233 $view_link = get_permalink( $source_id );
234
235 if ( false !== $view_link && '' !== $view_link ) {
236 $source_actions['view'] = sprintf(
237 '<a href="%s" rel="bookmark" aria-label="%s">%s</a>',
238 esc_url( (string) $view_link ),
239 /* translators: %s: Title of the source post the table belongs to. */
240 esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'table-builder-block' ), $title ) ),
241 esc_html__( 'View', 'table-builder-block' )
242 );
243 }
244 }
245 }
246
247 if ( current_user_can( 'delete_post', $source_id ) ) {
248 if ( $source_post instanceof WP_Post && 'trash' === $source_post->post_status ) {
249 $source_actions['delete'] = sprintf(
250 '<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
251 get_delete_post_link( $source_id ),
252 /* translators: %s: Title of the source post the table belongs to. */
253 esc_attr( sprintf( __( 'Delete &#8220;%s&#8221; permanently', 'table-builder-block' ), $title ) ),
254 esc_html__( 'Delete Permanently', 'table-builder-block' )
255 );
256 } elseif ( EMPTY_TRASH_DAYS ) {
257 $source_actions['trash'] = sprintf(
258 '<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
259 get_delete_post_link( $source_id ),
260 /* translators: %s: Title of the source post the table belongs to. */
261 esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash', 'table-builder-block' ), $title ) ),
262 esc_html_x( 'Trash', 'verb', 'table-builder-block' )
263 );
264 }
265
266 if ( $source_post instanceof WP_Post && ( 'trash' === $source_post->post_status || ! EMPTY_TRASH_DAYS ) ) {
267 $source_actions['delete'] = sprintf(
268 '<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
269 get_delete_post_link( $source_id ),
270 /* translators: %s: Title of the source post the table belongs to. */
271 esc_attr( sprintf( __( 'Delete &#8220;%s&#8221; permanently', 'table-builder-block' ), $title ) ),
272 esc_html__( 'Delete Permanently', 'table-builder-block' )
273 );
274 }
275 }
276
277 return empty( $source_actions ) ? $actions : $source_actions;
278 }
279
280 /**
281 * Enqueues the Tables admin list-table's CSS/JS on the "edit.php" screen
282 * for the Tables post type only.
283 *
284 * @param string $hook Current admin page hook suffix.
285 * @return void
286 */
287 public function enqueue_admin_assets( string $hook ): void {
288 if ( 'edit.php' !== $hook ) {
289 return;
290 }
291
292 $screen = get_current_screen();
293 if ( ! $screen || TableCPT::POST_TYPE !== $screen->post_type ) {
294 return;
295 }
296
297 $style_path = TABLE_BUILDER_BLOCK_PLUGIN_DIR . 'assets/css/table-cpt-admin.css';
298 $script_path = TABLE_BUILDER_BLOCK_PLUGIN_DIR . 'assets/js/table-cpt-admin.js';
299
300 wp_enqueue_style(
301 'tablekit-cpt-admin',
302 TABLE_BUILDER_BLOCK_PLUGIN_URL . 'assets/css/table-cpt-admin.css',
303 array(),
304 file_exists( $style_path ) ? (string) filemtime( $style_path ) : TABLE_BUILDER_BLOCK_PLUGIN_VERSION
305 );
306
307 wp_enqueue_script(
308 'tablekit-cpt-admin',
309 TABLE_BUILDER_BLOCK_PLUGIN_URL . 'assets/js/table-cpt-admin.js',
310 array(),
311 file_exists( $script_path ) ? (string) filemtime( $script_path ) : TABLE_BUILDER_BLOCK_PLUGIN_VERSION,
312 true
313 );
314
315 wp_localize_script(
316 'tablekit-cpt-admin',
317 'tablekitCptAdmin',
318 array(
319 'copiedText' => __( 'Copied!', 'table-builder-block' ),
320 'failedText' => __( 'Copy failed', 'table-builder-block' ),
321 )
322 );
323 }
324
325 /**
326 * Renders the "Shortcode" column: a copyable `[tableKit post_id="..."]` shortcode.
327 *
328 * @param WP_Post $post The row's post (real or virtual).
329 * @return void
330 */
331 private function render_column_shortcode( WP_Post $post ): void {
332 if ( ! empty( $post->tablekit_is_virtual ) ) {
333 $this->virtual_posts->render_shortcode_input(
334 sprintf( '[tableKit post_id="%d"]', (int) $post->tablekit_source_id )
335 );
336 return;
337 }
338
339 $this->virtual_posts->render_shortcode_input( sprintf( '[tableKit post_id="%d"]', (int) $post->ID ) );
340 }
341
342 /**
343 * Renders the "Child Shortcodes" column: a collapsible panel listing shortcodes
344 * for each child table block found in the row's post.
345 *
346 * @param WP_Post $post The row's post (real or virtual).
347 * @return void
348 */
349 private function render_column_child_shortcodes( WP_Post $post ): void {
350 $child_blocks = $this->get_child_shortcode_blocks( $post );
351
352 if ( empty( $child_blocks ) ) {
353 echo '&mdash;';
354 return;
355 }
356
357 $source_id = ! empty( $post->tablekit_is_virtual )
358 ? (int) $post->tablekit_source_id
359 : (int) $post->ID;
360
361 $this->virtual_posts->render_child_shortcode_panel( $child_blocks, (int) $post->ID, $source_id );
362 }
363
364 /**
365 * Renders the "Source" column, dispatching to the virtual- or real-post variant.
366 *
367 * @param WP_Post $post The row's post (real or virtual).
368 * @return void
369 */
370 private function render_column_source( WP_Post $post ): void {
371 if ( ! empty( $post->tablekit_is_virtual ) ) {
372 $this->render_virtual_source_cell( $post );
373 return;
374 }
375
376 $this->render_real_source_cell( $post );
377 }
378
379 /**
380 * Renders the "Source" column contents for a virtual row: a link to the
381 * source post plus a breakdown of block types/counts found in it.
382 *
383 * @param WP_Post $post The virtual row's post.
384 * @return void
385 */
386 private function render_virtual_source_cell( WP_Post $post ): void {
387 $source_type_obj = get_post_type_object( $post->tablekit_source_type );
388 $type_label = $source_type_obj
389 ? esc_html( $source_type_obj->labels->singular_name )
390 : esc_html( $post->tablekit_source_type );
391
392 $block_counts = $this->count_blocks_by_name( (array) $post->tablekit_blocks );
393
394 echo '<div class="tablekit-source-cell">';
395 printf(
396 '<strong>%s:</strong> <a href="%s" target="_blank">%s</a><br>',
397 $type_label, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already run through esc_html() above.
398 esc_url( (string) get_edit_post_link( $post->tablekit_source_id ) ),
399 esc_html( (string) $post->tablekit_source_title )
400 );
401 $this->render_block_type_list( $block_counts );
402 echo '</div>';
403 }
404
405 /**
406 * Renders the "Source" column contents for a real tablekit_table post: an
407 * "All Tables" label plus a breakdown of the table blocks it contains.
408 *
409 * @param WP_Post $post The real row's post.
410 * @return void
411 */
412 private function render_real_source_cell( WP_Post $post ): void {
413 echo '<div class="tablekit-source-cell">';
414 echo '<strong>' . esc_html__( 'All Tables', 'table-builder-block' ) . '</strong><br>';
415
416 $blocks = $this->get_child_shortcode_blocks( $post );
417
418 if ( empty( $blocks ) ) {
419 echo '<span style="color:#999;font-size:11px;">'
420 . esc_html__( 'No table blocks found', 'table-builder-block' )
421 . '</span>';
422 } else {
423 $this->render_block_type_list( $this->count_blocks_by_name( $blocks ) );
424 }
425
426 echo '</div>';
427 }
428
429 /**
430 * Renders the "Author" column: source post's author for virtual rows,
431 * or the post's own author otherwise.
432 *
433 * @param WP_Post $post The row's post (real or virtual).
434 * @return void
435 */
436 private function render_column_author( WP_Post $post ): void {
437 if ( ! empty( $post->tablekit_is_virtual ) ) {
438 $author = $post->tablekit_source_author;
439 echo esc_html( $author ? $author : '&mdash;' );
440 return;
441 }
442
443 $author_name = get_the_author_meta( 'display_name', $post->post_author );
444 echo esc_html( $author_name ? $author_name : '&mdash;' );
445 }
446
447 /**
448 * Gets the list of child table-block descriptors for a row's post (from its
449 * cached virtual data, or freshly scanned for a real post).
450 *
451 * @param WP_Post $post The row's post (real or virtual).
452 * @return array Block descriptors with block_name/block_id/shortcode_id keys.
453 */
454 private function get_child_shortcode_blocks( WP_Post $post ): array {
455 if ( ! empty( $post->tablekit_is_virtual ) && ! empty( $post->tablekit_blocks ) ) {
456 return (array) $post->tablekit_blocks;
457 }
458
459 $matched = $this->scanner->extract_table_blocks( parse_blocks( (string) $post->post_content ) );
460 $blocks = array();
461
462 foreach ( $matched as $block ) {
463 $attrs = $block['attrs'] ?? array();
464 $block_id = (string) ( $attrs['blockID'] ?? '' );
465
466 $blocks[] = array(
467 'block_name' => (string) ( $block['blockName'] ?? '' ),
468 'block_id' => $block_id,
469 'shortcode_id' => $this->scanner->resolve_shortcode_id( $attrs, $block_id ),
470 );
471 }
472
473 return $blocks;
474 }
475
476 /**
477 * Renders a list of block-type labels with their counts (e.g. "-> Data Table block ×2").
478 *
479 * @param array $block_counts Block counts keyed by block name, from count_blocks_by_name().
480 * @return void
481 */
482 private function render_block_type_list( array $block_counts ): void {
483 foreach ( $block_counts as $block_name => $count ) {
484 echo '<span class="tablekit-source-block">-&gt; ' . esc_html( TableCPT::get_block_label( $block_name ) );
485
486 if ( $count > 1 ) {
487 printf( ' <strong class="tablekit-source-block-count">&times;%d</strong>', $count ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- %d forces integer output.
488 }
489
490 echo '</span>';
491 }
492 }
493
494 /**
495 * Tallies block descriptors by block name.
496 *
497 * @param array $blocks Block descriptors with a "block_name" key.
498 * @return array<string,int> Counts keyed by block name.
499 */
500 private function count_blocks_by_name( array $blocks ): array {
501 $counts = array();
502
503 foreach ( $blocks as $block ) {
504 $name = (string) ( $block['block_name'] ?? '' );
505
506 if ( '' !== $name ) {
507 $counts[ $name ] = ( $counts[ $name ] ?? 0 ) + 1;
508 }
509 }
510
511 return $counts;
512 }
513
514 /**
515 * Whether the Pro add-on plugin is active.
516 *
517 * @return bool
518 */
519 private function is_pro_active(): bool {
520 return defined( 'TABLE_BUILDER_BLOCK_PRO_PLUGIN_VERSION' );
521 }
522 }
523