Skip to content

Commit 56545d1

Browse files
committed
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
1 parent 1173551 commit 56545d1

4 files changed

Lines changed: 591 additions & 53 deletions

File tree

classes/Visualizer/Render/Library.php

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

31+
/** @var bool|null Cached result of _isListView() to avoid repeat DB reads per request. */
32+
private $_list_view_cached = null;
33+
3134
/**
3235
* Renders library page.
3336
*
@@ -78,6 +81,8 @@ private function getDisplayForm() {
7881
echo '<div class="visualizer-library-form">
7982
<form action="' . admin_url( 'admin.php' ) . '">
8083
<input type="hidden" name="page" value="' . Visualizer_Plugin::NAME . '"/>
84+
<input type="hidden" name="view" value="' . esc_attr( $this->_isListView() ? 'list' : 'grid' ) . '"/>
85+
<span class="viz-view-toggle-group">' . $this->_getViewToggleHTML() . '</span>
8186
<select class="viz-filter" name="type">
8287
';
8388

@@ -282,34 +287,58 @@ private function _renderLibrary() {
282287
echo '<div id="visualizer-content-wrapper">';
283288
echo '<div id="tsdk_banner" class="visualizer-banner"></div>';
284289
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;
290+
if ( $this->_isListView() ) {
291+
echo '<div id="visualizer-library" class="visualizer-clearfix view-list">';
292+
echo '<table class="wp-list-table widefat striped viz-charts-table">';
293+
echo '<thead><tr>';
294+
echo '<th class="col-id">' . esc_html__( 'ID', 'visualizer' ) . '</th>';
295+
echo '<th class="col-title">' . esc_html__( 'Title', 'visualizer' ) . '</th>';
296+
echo '<th class="col-type">' . esc_html__( 'Type', 'visualizer' ) . '</th>';
297+
echo '<th class="col-shortcode">' . esc_html__( 'Shortcode', 'visualizer' ) . '</th>';
298+
echo '<th class="col-actions">' . esc_html__( 'Actions', 'visualizer' ) . '</th>';
299+
echo '</tr></thead><tbody>';
300+
foreach ( $this->charts as $placeholder_id => $chart ) {
301+
$enable_controls = false;
302+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
303+
if ( ! empty( $settings['controls']['controlType'] ) ) {
304+
$column_index = $settings['controls']['filterColumnIndex'];
305+
$column_label = $settings['controls']['filterColumnLabel'];
306+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
307+
$enable_controls = true;
308+
}
297309
}
298-
}
299-
if ( 3 === $count ) {
300-
$this->_renderSidebar();
301-
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
302-
} else {
303310
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
304311
}
305-
}
306-
// show the sidebar if there are less than 3 charts.
307-
if ( $count < 3 ) {
312+
echo '</tbody></table>';
308313
$this->_renderSidebar();
314+
echo '</div>';
315+
} else {
316+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
317+
$count = 0;
318+
foreach ( $this->charts as $placeholder_id => $chart ) {
319+
// show the sidebar after the first 3 charts.
320+
++$count;
321+
$enable_controls = false;
322+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
323+
if ( ! empty( $settings['controls']['controlType'] ) ) {
324+
$column_index = $settings['controls']['filterColumnIndex'];
325+
$column_label = $settings['controls']['filterColumnLabel'];
326+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
327+
$enable_controls = true;
328+
}
329+
}
330+
if ( 3 === $count ) {
331+
$this->_renderSidebar();
332+
}
333+
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
334+
}
335+
if ( $count < 3 ) {
336+
$this->_renderSidebar();
337+
}
338+
echo '</div>';
309339
}
310-
echo '</div>';
311340
} else {
312-
echo '<div id="visualizer-library" class="visualizer-clearfix">';
341+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
313342
echo '<div class="items"><div class="visualizer-chart">';
314343
echo '<div class="visualizer-chart-canvas visualizer-nochart-canvas">';
315344
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>';
@@ -413,7 +442,28 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
413442
$chart_status['title'] = __( 'Click to view the error', 'visualizer' );
414443
}
415444
$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>';
445+
446+
if ( $this->_isListView() ) {
447+
// ── List view: table row ──
448+
echo '<tr class="viz-list-row">';
449+
echo '<td class="col-id">#' . esc_html( $chart_id ) . '</td>';
450+
echo '<td class="col-title">' . esc_html( $title ) . '</td>';
451+
echo '<td class="col-type">' . ( ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '&mdash;' ) . '</td>';
452+
echo '<td class="col-shortcode"><code class="viz-shortcode-display">' . esc_html( $shortcode ) . '</code></td>';
453+
echo '<td class="col-actions"><div class="visualizer-action-group">';
454+
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>';
455+
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>';
456+
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>';
457+
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>';
458+
echo '<a class="visualizer-chart-action visualizer-chart-edit ' . esc_attr( $pro_class ) . '" href="javascript:;" data-chart="' . esc_attr( $chart_id ) . '"><span class="dashicons dashicons-admin-generic ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Edit', 'visualizer' ) . '</span></a>';
459+
echo '</div></td>';
460+
echo '</tr>';
461+
return;
462+
}
463+
464+
// ── Grid view: card ──
465+
$type_badge = ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '';
466+
echo '<div class="items"><div class="visualizer-chart"><div class="visualizer-chart-title"><span>' . esc_html( $title ) . '</span>' . $type_badge . '</div>';
417467
if ( Visualizer_Module::is_pro() && $with_filter ) {
418468
echo '<div id="chart_wrapper_' . $placeholder_id . '">';
419469
echo '<div id="control_wrapper_' . $placeholder_id . '" class="vz-library-chart-filter"></div>';
@@ -426,51 +476,76 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
426476
}
427477
echo '<div class="visualizer-chart-footer visualizer-clearfix">';
428478
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>';
479+
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>';
480+
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>';
431481
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>';
482+
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>';
433483
}
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>';
484+
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>';
485+
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>';
486+
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_html__( 'Edit', 'visualizer' ) . '</span></a>';
437487
echo '</div>';
438488
do_action( 'visualizer_chart_languages', $chart_id );
439489
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>';
440490
echo '</div>';
441491
echo '</div></div>';
442492
}
443493

494+
/**
495+
* Returns true when the library should render in list (no-preview) mode.
496+
*
497+
* Priority: ?view= URL param (saves to user meta) → saved user meta → grid default.
498+
*
499+
* No nonce needed: this is a bookmarkable UI preference URL. A nonce would expire
500+
* and break saved/shared links for zero real security gain — the value is allowlisted
501+
* to 'list'|'grid' before any write happens.
502+
*/
503+
private function _isListView() {
504+
if ( null !== $this->_list_view_cached ) {
505+
return $this->_list_view_cached;
506+
}
507+
if ( isset( $_GET['view'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
508+
$view = sanitize_text_field( wp_unslash( $_GET['view'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
509+
if ( in_array( $view, array( 'list', 'grid' ), true ) ) {
510+
update_user_meta( get_current_user_id(), 'visualizer_library_view', $view );
511+
}
512+
$this->_list_view_cached = ( 'list' === $view );
513+
} else {
514+
$saved = get_user_meta( get_current_user_id(), 'visualizer_library_view', true );
515+
$this->_list_view_cached = ( 'list' === $saved );
516+
}
517+
return $this->_list_view_cached;
518+
}
519+
520+
/**
521+
* Returns the HTML for the grid/list view toggle links.
522+
*/
523+
private function _getViewToggleHTML() {
524+
$is_list = $this->_isListView();
525+
$grid_url = esc_url( add_query_arg( 'view', 'grid' ) );
526+
$list_url = esc_url( add_query_arg( 'view', 'list' ) );
527+
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>'
528+
. '<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>';
529+
}
530+
444531
/**
445532
* Render 2-col sidebar
446533
*/
447534
private function _renderSidebar() {
448535
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::proFeaturesLocked() ) {
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>';
536+
$upgrade_url = tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'sidebarMenuUpgrade', 'index' );
537+
$chart_types = Visualizer_Module_Admin::proFeaturesLocked() ? __( '6 more chart types', 'visualizer' ) : __( '11 more chart types', 'visualizer' );
538+
echo '<div class="items items--upsell">';
539+
echo '<div class="viz-upsell-banner">';
540+
echo '<span class="dashicons dashicons-star-filled viz-upsell-banner__icon"></span>';
541+
echo '<div class="viz-upsell-banner__text">';
542+
echo '<strong>' . esc_html__( 'Unlock the full power of Visualizer PRO!', 'visualizer' ) . '</strong>';
543+
/* translators: %s: number of additional chart types (e.g. "11 more chart types") */
544+
echo '<span>' . sprintf( esc_html__( '%s, periodic data sync, database queries, frontend editor, and more.', 'visualizer' ), esc_html( $chart_types ) ) . '</span>';
473545
echo '</div>';
546+
echo '<div class="viz-upsell-banner__actions">';
547+
echo '<a href="' . esc_url( $upgrade_url . '#pro-features' ) . '" target="_blank" class="button button-secondary">' . esc_html__( 'View Features', 'visualizer' ) . '</a>';
548+
echo '<a href="' . esc_url( $upgrade_url . '#pricing' ) . '" target="_blank" class="button button-primary">' . esc_html__( 'Upgrade Now', 'visualizer' ) . '</a>';
474549
echo '</div>';
475550
echo '</div>';
476551
echo '</div>';

0 commit comments

Comments
 (0)