Skip to content

Commit b92baf9

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 b92baf9

4 files changed

Lines changed: 595 additions & 53 deletions

File tree

classes/Visualizer/Render/Library.php

Lines changed: 132 additions & 53 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,58 @@ 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+
echo '<table class="wp-list-table widefat striped viz-charts-table">';
297+
echo '<thead><tr>';
298+
echo '<th class="col-id">' . esc_html__( 'ID', 'visualizer' ) . '</th>';
299+
echo '<th class="col-title">' . esc_html__( 'Title', 'visualizer' ) . '</th>';
300+
echo '<th class="col-type">' . esc_html__( 'Type', 'visualizer' ) . '</th>';
301+
echo '<th class="col-shortcode">' . esc_html__( 'Shortcode', 'visualizer' ) . '</th>';
302+
echo '<th class="col-actions">' . esc_html__( 'Actions', 'visualizer' ) . '</th>';
303+
echo '</tr></thead><tbody>';
304+
foreach ( $this->charts as $placeholder_id => $chart ) {
305+
$enable_controls = false;
306+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
307+
if ( ! empty( $settings['controls']['controlType'] ) ) {
308+
$column_index = $settings['controls']['filterColumnIndex'];
309+
$column_label = $settings['controls']['filterColumnLabel'];
310+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
311+
$enable_controls = true;
312+
}
297313
}
298-
}
299-
if ( 3 === $count ) {
300-
$this->_renderSidebar();
301-
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
302-
} else {
303314
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
304315
}
305-
}
306-
// show the sidebar if there are less than 3 charts.
307-
if ( $count < 3 ) {
316+
echo '</tbody></table>';
308317
$this->_renderSidebar();
318+
echo '</div>';
319+
} else {
320+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
321+
$count = 0;
322+
foreach ( $this->charts as $placeholder_id => $chart ) {
323+
// show the sidebar after the first 3 charts.
324+
++$count;
325+
$enable_controls = false;
326+
$settings = isset( $chart['settings'] ) ? $chart['settings'] : array();
327+
if ( ! empty( $settings['controls']['controlType'] ) ) {
328+
$column_index = $settings['controls']['filterColumnIndex'];
329+
$column_label = $settings['controls']['filterColumnLabel'];
330+
if ( 'false' !== $column_index || 'false' !== $column_label ) {
331+
$enable_controls = true;
332+
}
333+
}
334+
if ( 3 === $count ) {
335+
$this->_renderSidebar();
336+
}
337+
$this->_renderChartBox( $placeholder_id, $chart['id'], $enable_controls );
338+
}
339+
if ( $count < 3 ) {
340+
$this->_renderSidebar();
341+
}
342+
echo '</div>';
309343
}
310-
echo '</div>';
311344
} else {
312-
echo '<div id="visualizer-library" class="visualizer-clearfix">';
345+
echo '<div id="visualizer-library" class="visualizer-clearfix view-grid">';
313346
echo '<div class="items"><div class="visualizer-chart">';
314347
echo '<div class="visualizer-chart-canvas visualizer-nochart-canvas">';
315348
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 +446,28 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
413446
$chart_status['title'] = __( 'Click to view the error', 'visualizer' );
414447
}
415448
$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>';
449+
450+
if ( $this->_isListView() ) {
451+
// ── List view: table row ──
452+
echo '<tr class="viz-list-row">';
453+
echo '<td class="col-id">#' . esc_html( (string) $chart_id ) . '</td>';
454+
echo '<td class="col-title">' . esc_html( $title ) . '</td>';
455+
echo '<td class="col-type">' . ( ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '&mdash;' ) . '</td>';
456+
echo '<td class="col-shortcode"><code class="viz-shortcode-display">' . esc_html( $shortcode ) . '</code></td>';
457+
echo '<td class="col-actions"><div class="visualizer-action-group">';
458+
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>';
459+
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>';
460+
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>';
461+
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>';
462+
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-admin-generic ' . esc_attr( $pro_class ) . '"></span><span class="tooltip-text">' . esc_html__( 'Edit', 'visualizer' ) . '</span></a>';
463+
echo '</div></td>';
464+
echo '</tr>';
465+
return;
466+
}
467+
468+
// ── Grid view: card ──
469+
$type_badge = ! empty( $chart_type ) ? '<span class="viz-chart-type-badge">' . esc_html( $chart_type ) . '</span>' : '';
470+
echo '<div class="items"><div class="visualizer-chart"><div class="visualizer-chart-title"><span>' . esc_html( $title ) . '</span>' . $type_badge . '</div>';
417471
if ( Visualizer_Module::is_pro() && $with_filter ) {
418472
echo '<div id="chart_wrapper_' . $placeholder_id . '">';
419473
echo '<div id="control_wrapper_' . $placeholder_id . '" class="vz-library-chart-filter"></div>';
@@ -426,51 +480,76 @@ private function _renderChartBox( $placeholder_id, $chart_id, $with_filter = fal
426480
}
427481
echo '<div class="visualizer-chart-footer visualizer-clearfix">';
428482
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>';
483+
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>';
484+
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>';
431485
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>';
486+
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>';
433487
}
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>';
488+
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>';
489+
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>';
490+
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>';
437491
echo '</div>';
438492
do_action( 'visualizer_chart_languages', $chart_id );
439493
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>';
440494
echo '</div>';
441495
echo '</div></div>';
442496
}
443497

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

0 commit comments

Comments
 (0)