Skip to content

Commit 9707d76

Browse files
feat: add list view toggle to chart library with user meta persistence (#1285)
* feat: add list view toggle to chart library with user meta persistence Adds a grid/list view toggle to the Visualizer library page. List view renders charts in a WP-style table. The selected view is saved to user meta so it persists across visits without a redirect. - List view: WP table layout, no canvas rendering, tooltips on actions - Shortcode cell is clickable to copy to clipboard with visual feedback - View preference saved via update_user_meta() with allowlist validation - 10 Playwright e2e tests covering toggle, persistence, and table content * fix: qa issues * chore: fix qa * chore fix: qa
1 parent 19a87cf commit 9707d76

4 files changed

Lines changed: 586 additions & 54 deletions

File tree

classes/Visualizer/Render/Library.php

Lines changed: 125 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
*/
2929
class Visualizer_Render_Library extends Visualizer_Render {
3030

31+
/**
32+
* Cached result of _isListView() to avoid repeat DB reads per request.
33+
*
34+
* @var bool|null
35+
*/
36+
private $_list_view_cached = null;
37+
3138
/**
3239
* Renders library page.
3340
*
@@ -78,6 +85,8 @@ private function getDisplayForm() {
7885
echo '<div class="visualizer-library-form">
7986
<form action="' . admin_url( 'admin.php' ) . '">
8087
<input type="hidden" name="page" value="' . Visualizer_Plugin::NAME . '"/>
88+
<input type="hidden" name="view" value="' . esc_attr( $this->_isListView() ? 'list' : 'grid' ) . '"/>
89+
<span class="viz-view-toggle-group">' . $this->_getViewToggleHTML() . '</span>
8190
<select class="viz-filter" name="type">
8291
';
8392

@@ -282,34 +291,50 @@ private function _renderLibrary() {
282291
echo '<div id="visualizer-content-wrapper">';
283292
echo '<div id="tsdk_banner" class="visualizer-banner"></div>';
284293
if ( ! empty( $this->charts ) ) {
285-
echo '<div id="visualizer-library" class="visualizer-clearfix">';
286-
$count = 0;
287-
foreach ( $this->charts as $placeholder_id => $chart ) {
288-
// show the sidebar after the first 3 charts.
289-
++$count;
290-
$enable_controls = false;
291-
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
292-
if ( ! empty( $settings['controls']['controlType'] ) ) {
293-
$column_index = $settings['controls']['filterColumnIndex'];
294-
$column_label = $settings['controls']['filterColumnLabel'];
295-
if ( 'false' !== $column_index || 'false' !== $column_label ) {
296-
$enable_controls = true;
294+
if ( $this->_isListView() ) {
295+
echo '<div id="visualizer-library" class="visualizer-clearfix view-list">';
296+
$this->_renderSidebar();
297+
echo '<table class="wp-list-table widefat striped viz-charts-table">';
298+
echo '<thead><tr>';
299+
echo '<th class="col-id">' . esc_html__( 'ID', 'visualizer' ) . '</th>';
300+
echo '<th class="col-title">' . esc_html__( 'Title', 'visualizer' ) . '</th>';
301+
echo '<th class="col-type">' . esc_html__( 'Type', 'visualizer' ) . '</th>';
302+
echo '<th class="col-shortcode">' . esc_html__( 'Shortcode', 'visualizer' ) . '</th>';
303+
echo '<th class="col-actions">' . esc_html__( 'Actions', 'visualizer' ) . '</th>';
304+
echo '</tr></thead><tbody>';
305+
foreach ( $this->charts as $placeholder_id => $chart ) {
306+
$enable_controls = false;
307+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
308+
if ( ! empty( $settings['controls']['controlType'] ) ) {
309+
$column_index = $settings['controls']['filterColumnIndex'];
310+
$column_label = $settings['controls']['filterColumnLabel'];
311+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
312+
$enable_controls = true;
313+
}
297314
}
298-
}
299-
if ( 3 === $count ) {
300-
$this->_renderSidebar();
301-
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
302-
} else {
303315
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
304316
}
305-
}
306-
// show the sidebar if there are less than 3 charts.
307-
if ( $count < 3 ) {
317+
echo '</tbody></table>';
318+
echo '</div>';
319+
} else {
320+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
308321
$this->_renderSidebar();
322+
foreach ( $this->charts as $placeholder_id => $chart ) {
323+
$enable_controls = false;
324+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
325+
if ( ! empty( $settings['controls']['controlType'] ) ) {
326+
$column_index = $settings['controls']['filterColumnIndex'];
327+
$column_label = $settings['controls']['filterColumnLabel'];
328+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
329+
$enable_controls = true;
330+
}
331+
}
332+
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
333+
}
334+
echo '</div>';
309335
}
310-
echo '</div>';
311336
} else {
312-
echo '<div id="visualizer-library" class="visualizer-clearfix">';
337+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
313338
echo '<div class="items"><div class="visualizer-chart">';
314339
echo '<div class="visualizer-chart-canvas visualizer-nochart-canvas">';
315340
echo '<div class="visualizer-notfound">', esc_html__( 'No charts found', 'visualizer' ), '<p><h2><a href="javascript:;" class="add-new-h2 add-new-chart">', esc_html__( 'Add New', 'visualizer' ), '</a></h2></p></div>';
@@ -321,7 +346,7 @@ private function _renderLibrary() {
321346
echo '<span class="visualizer-chart-action visualizer-nochart-image"><span class="dashicons dashicons-format-image"></span></span>';
322347
echo '<span class="visualizer-chart-action visualizer-nochart-export"><span class="dashicons dashicons-download"></span></span>';
323348
echo '<span class="visualizer-chart-action visualizer-nochart-clone"><span class="dashicons dashicons-admin-page"></span></span>';
324-
echo '<span class="visualizer-chart-action visualizer-nochart-edit"><span class="dashicons dashicons-admin-generic"></span></span>';
349+
echo '<span class="visualizer-chart-action visualizer-nochart-edit"><span class="dashicons dashicons-edit"></span></span>';
325350
echo '</div>';
326351
echo '</div>';
327352
echo '</div></div>';
@@ -413,7 +438,28 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
413438
$chart_status['title'] = __( 'Click to view the error', 'visualizer' );
414439
}
415440
$shortcode = sprintf( '[visualizer id="%s" class=""]', $chart_id );
416-
echo '<div class="items"><div class="visualizer-chart"><div class="visualizer-chart-title">', esc_html( $title ), '</div>';
441+
442+
if ( $this->_isListView() ) {
443+
// ── List view: table row ──
444+
echo '<tr class="viz-list-row">';
445+
echo '<td class="col-id">#' . esc_html( (string) $chart_id ) . '</td>';
446+
echo '<td class="col-title">' . esc_html( $title ) . '</td>';
447+
echo '<td class="col-type">' . ( ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '&mdash;' ) . '</td>';
448+
echo '<td class="col-shortcode"><code class="viz-shortcode-display">' . esc_html( $shortcode ) . '</code></td>';
449+
echo '<td class="col-actions"><div class="visualizer-action-group">';
450+
echo '<a class="visualizer-chart-action visualizer-chart-delete" href="' . $delete_url . '" onclick="return showNotice.warn();"><span class="dashicons dashicons-trash"></span><span class="tooltip-text">' . esc_html__( 'Delete', 'visualizer' ) . '</span></a>';
451+
echo '<a class="visualizer-chart-action visualizer-chart-shortcode ' . esc_attr( $pro_class ) . '" href="javascript:;" data-clipboard-text="' . esc_attr( $shortcode ) . '"><span class="dashicons dashicons-shortcode ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Copy Shortcode', 'visualizer' ) . '</span></a>';
452+
echo '<a class="visualizer-chart-action visualizer-chart-export ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="' . $export_link . '"><span class="dashicons dashicons-download ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Export CSV', 'visualizer' ) . '</span></a>';
453+
echo '<a class="visualizer-chart-action visualizer-chart-clone ' . esc_attr( $pro_class ) . '" href="' . $clone_url . '"><span class="dashicons dashicons-admin-page ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Duplicate', 'visualizer' ) . '</span></a>';
454+
echo '<a class="visualizer-chart-action visualizer-chart-edit ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="' . esc_attr( (string) $chart_id ) . '"><span class="dashicons dashicons-edit ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Edit', 'visualizer' ) . '</span></a>';
455+
echo '</div></td>';
456+
echo '</tr>';
457+
return;
458+
}
459+
460+
// ── Grid view: card ──
461+
$type_badge = ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '';
462+
echo '<div class="items"><div class="visualizer-chart"><div class="visualizer-chart-title"><span>' . esc_html( $title ) . '</span>' . $type_badge . '</div>';
417463
if ( Visualizer_Module::is_pro() && $with_filter ) {
418464
echo '<div id="chart_wrapper_' . $placeholder_id . '">';
419465
echo '<div id="control_wrapper_' . $placeholder_id . '" class="vz-library-chart-filter"></div>';
@@ -426,51 +472,76 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
426472
}
427473
echo '<div class="visualizer-chart-footer visualizer-clearfix">';
428474
echo '<div class="visualizer-action-group">';
429-
echo '<a class="visualizer-chart-action visualizer-chart-delete" href="', $delete_url, '" onclick="return showNotice.warn();"><span class="dashicons dashicons-trash"></span><span class="tooltip-text">' . esc_attr__( 'Delete', 'visualizer' ) . '</span></a>';
430-
echo '<a class="visualizer-chart-action visualizer-chart-shortcode ' . esc_attr( $pro_class ) . '" href="javascript:;" data-clipboard-text="', esc_attr( $shortcode ), '"><span class="dashicons dashicons-shortcode ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_attr__( 'Copy Shortcode', 'visualizer' ) . '</span></a>';
475+
echo '<a class="visualizer-chart-action visualizer-chart-delete" href="', $delete_url, '" onclick="return showNotice.warn();"><span class="dashicons dashicons-trash"></span><span class="tooltip-text">' . esc_html__( 'Delete', 'visualizer' ) . '</span></a>';
476+
echo '<a class="visualizer-chart-action visualizer-chart-shortcode ' . esc_attr( $pro_class ) . '" href="javascript:;" data-clipboard-text="', esc_attr( $shortcode ), '"><span class="dashicons dashicons-shortcode ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Copy Shortcode', 'visualizer' ) . '</span></a>';
431477
if ( $this->can_chart_have_action( 'image', $chart_id ) ) {
432-
echo '<a class="visualizer-chart-action visualizer-chart-image ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="visualizer-', $chart_id, '" data-chart-title="', $title, '"><span class="dashicons dashicons-format-image ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_attr__( 'Download PNG', 'visualizer' ) . '</span></a>';
478+
echo '<a class="visualizer-chart-action visualizer-chart-image ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="visualizer-', $chart_id, '" data-chart-title="', $title, '"><span class="dashicons dashicons-format-image ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Download PNG', 'visualizer' ) . '</span></a>';
433479
}
434-
echo '<a class="visualizer-chart-action visualizer-chart-export ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="', $export_link, '"><span class="dashicons dashicons-download ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_attr__( 'Export CSV', 'visualizer' ) . '</span></a>';
435-
echo '<a class="visualizer-chart-action visualizer-chart-clone ' . esc_attr( $pro_class ) . '" href="', $clone_url, '"><span class="dashicons dashicons-admin-page ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_attr__( 'Duplicate', 'visualizer' ) . '</span></a>';
436-
echo '<a class="visualizer-chart-action visualizer-chart-edit ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="', $chart_id, '"><span class="dashicons dashicons-admin-generic ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_attr__( 'Edit', 'visualizer' ) . '</span></a>';
480+
echo '<a class="visualizer-chart-action visualizer-chart-export ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="', $export_link, '"><span class="dashicons dashicons-download ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Export CSV', 'visualizer' ) . '</span></a>';
481+
echo '<a class="visualizer-chart-action visualizer-chart-clone ' . esc_attr( $pro_class ) . '" href="', $clone_url, '"><span class="dashicons dashicons-admin-page ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Duplicate', 'visualizer' ) . '</span></a>';
482+
echo '<a class="visualizer-chart-action visualizer-chart-edit ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="', $chart_id, '"><span class="dashicons dashicons-edit ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Edit', 'visualizer' ) . '</span></a>';
437483
echo '</div>';
438484
do_action( 'visualizer_chart_languages', $chart_id );
439485
echo '<hr><div class="visualizer-chart-status"><span title="' . __( 'Chart ID', 'visualizer' ) . '">(' . $chart_id . '):</span> <span class="visualizer-date" title="' . __( 'Last Updated', 'visualizer' ) . '">' . $chart_status['date'] . '</span><span class="visualizer-error"><i class="dashicons ' . $chart_status['icon'] . '" data-viz-error="' . esc_attr( str_replace( '"', "'", $chart_status['error'] ) ) . '" title="' . esc_attr( $chart_status['title'] ) . '"></i></span></div>';
440486
echo '</div>';
441487
echo '</div></div>';
442488
}
443489

490+
/**
491+
* Returns true when the library should render in list (no-preview) mode.
492+
*
493+
* Priority: ?view= URL param (saves to user meta) → saved user meta → grid default.
494+
*
495+
* No nonce needed: this is a bookmarkable UI preference URL. A nonce would expire
496+
* and break saved/shared links for zero real security gain — the value is allowlisted
497+
* to 'list'|'grid' before any write happens.
498+
*/
499+
private function _isListView(): bool {
500+
if ( null !== $this->_list_view_cached ) {
501+
return $this->_list_view_cached;
502+
}
503+
if ( isset( $_GET['view'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
504+
$view = sanitize_text_field( wp_unslash( $_GET['view'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
505+
if ( in_array( $view, array( 'list', 'grid' ), true ) ) {
506+
update_user_meta( get_current_user_id(), 'visualizer_library_view', $view );
507+
}
508+
$this->_list_view_cached = ( 'list' === $view );
509+
} else {
510+
$saved = get_user_meta( get_current_user_id(), 'visualizer_library_view', true );
511+
$this->_list_view_cached = ( 'list' === $saved );
512+
}
513+
return $this->_list_view_cached;
514+
}
515+
516+
/**
517+
* Returns the HTML for the grid/list view toggle links.
518+
*/
519+
private function _getViewToggleHTML(): string {
520+
$is_list = $this->_isListView();
521+
$grid_url = esc_url( add_query_arg( 'view', 'grid' ) );
522+
$list_url = esc_url( add_query_arg( 'view', 'list' ) );
523+
return '<a href="' . $grid_url . '" class="viz-view-toggle' . ( ! $is_list ? ' active' : '' ) . '" title="' . esc_attr__( 'Grid View', 'visualizer' ) . '"><span class="dashicons dashicons-screenoptions"></span></a>'
524+
. '<a href="' . $list_url . '" class="viz-view-toggle' . ( $is_list ? ' active' : '' ) . '" title="' . esc_attr__( 'List View', 'visualizer' ) . '"><span class="dashicons dashicons-list-view"></span></a>';
525+
}
526+
444527
/**
445528
* Render 2-col sidebar
446529
*/
447530
private function _renderSidebar() {
448531
if ( ! Visualizer_Module::is_pro() ) {
449-
echo '<div class="items">';
450-
echo '<div class="viz-pro">';
451-
echo '<div id="visualizer-sidebar" class="one-columns">';
452-
echo '<div class="visualizer-sidebar-box">';
453-
echo '<h3>' . __( 'Discover the power of PRO!', 'visualizer' ) . '</h3><ul>';
454-
if ( Visualizer_Module_Admin::proFeaturesEnabled() ) {
455-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( '6 more chart types', 'visualizer' );
456-
} else {
457-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( '11 more chart types', 'visualizer' ) . '</li>';
458-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Synchronize Data Periodically', 'visualizer' ) . '</li>';
459-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'ChartJS Charts', 'visualizer' ) . '</li>';
460-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Table Google chart', 'visualizer' ) . '</li>';
461-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Frontend Actions(Print, Export, Copy, Download)', 'visualizer' ) . '</li>';
462-
}
463-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Spreadsheet like editor', 'visualizer' ) . '</li>';
464-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Import from other charts', 'visualizer' ) . '</li>';
465-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Use database query to create charts', 'visualizer' ) . '</li>';
466-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Create charts from WordPress tables', 'visualizer' ) . '</li>';
467-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Frontend editor', 'visualizer' ) . '</li>';
468-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Private charts', 'visualizer' ) . '</li>';
469-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'WPML support for translating charts', 'visualizer' ) . '</li>';
470-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Integration with Woocommerce Data endpoints', 'visualizer' ) . '</li>';
471-
echo '<li><svg class="icon list-icon"><use xlink:href="#list-icon"></use></svg>' . __( 'Auto-sync with online files', 'visualizer' ) . '</li></ul>';
472-
echo '<p class="vz-sidebar-box-action"><a href="' . tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'sidebarMenuUpgrade', 'index' ) . '#pro-features" target="_blank" class="button button-secondary">' . __( 'View more features', 'visualizer' ) . '</a><a href="' . tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'sidebarMenuUpgrade', 'index' ) . '#pricing" target="_blank" class="button button-primary">' . __( 'Upgrade Now', 'visualizer' ) . '</a></p>';
532+
$upgrade_url = tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'sidebarMenuUpgrade', 'index' );
533+
$chart_types = Visualizer_Module_Admin::proFeaturesEnabled() ? __( '6 more chart types', 'visualizer' ) : __( '11 more chart types', 'visualizer' );
534+
echo '<div class="items--upsell">';
535+
echo '<div class="viz-upsell-banner">';
536+
echo '<span class="dashicons dashicons-star-filled viz-upsell-banner__icon"></span>';
537+
echo '<div class="viz-upsell-banner__text">';
538+
echo '<strong>' . esc_html__( 'Unlock the full power of Visualizer PRO!', 'visualizer' ) . '</strong>';
539+
/* translators: %s: number of additional chart types (e.g. "11 more chart types") */
540+
echo '<span>' . sprintf( esc_html__( '%s, periodic data sync, database queries, frontend editor, and more.', 'visualizer' ), esc_html( $chart_types ) ) . '</span>';
473541
echo '</div>';
542+
echo '<div class="viz-upsell-banner__actions">';
543+
echo '<a href="' . esc_url( $upgrade_url . '#pro-features' ) . '" target="_blank" class="button button-secondary">' . esc_html__( 'View Features', 'visualizer' ) . '</a>';
544+
echo '<a href="' . esc_url( $upgrade_url . '#pricing' ) . '" target="_blank" class="button button-primary">' . esc_html__( 'Upgrade Now', 'visualizer' ) . '</a>';
474545
echo '</div>';
475546
echo '</div>';
476547
echo '</div>';

0 commit comments

Comments
 (0)