Skip to content
Gravity Tables
patch v4.1.51-4.1.59 · · 8 min read

Nine quiet quality fixes, the kind nobody notices until they break

Nine versions in two weeks, all addressing failure modes that show up in specific stacks: i18n strings invisible to translators, anchor tags double-escaped, multi-table pages misbehaving, bulk-delete capability checks, TinyMCE smart-quote breaking shortcodes, jQuery handler accumulation, CSS cascade ordering, window.resize handler stacking, and a CSP-strict tightening. Marketing rarely talks about this work; doing it is what keeps a plugin alive past version 1.

  • 4.1.59, i18n: 53+ untranslated strings now go through __() + missing languages/ directory created
  • 4.1.58, anchor tags in cells stop double-escaping into visible <a href=> text
  • 4.1.57, two-tier cache eliminates duplicate DB hits on multi-instance pages
  • 4.1.55, TinyMCE classic-editor integration + no_texturize_shortcodes prevents smart-quote breakage
  • 4.1.51, CSP-strict: all inline scripts now nonce-tagged via wp_add_inline_script

Two weeks, nine versions, zero new features. Every entry in this post is a fix for something that worked in 99% of installs, but in the 1% where it didn’t, it really didn’t.

The pattern is consistent: a customer reports a specific symptom, we trace it to a root cause that touches a few files, the fix lands in the next nightly. Most of these would be invisible if you read the changelog as a feature catalog. Read as a craftsmanship log, they tell a different story: the plugin is being maintained.

v4.1.59, i18n: 53+ untranslated strings + missing languages/ directory#

Two compounding bugs that made translation effectively impossible:

  1. 53+ wp_send_json_error() / wp_send_json_success() / wp_die() calls in class-gt-ajax.php, class-gt-debug.php, and class-gt-woocommerce.php were passing bare string literals like 'Permission denied' or 'Form not found' instead of __('Permission denied', 'gravity-tables'). The strings never appeared in the .pot file, translators couldn’t translate what they couldn’t see.
  2. The plugin header declared Domain Path: /languages but the languages/ directory didn’t exist in the repo. WordPress looks at that path for .pot / .po / .mo files; missing directory meant no translations would load even after load_plugin_textdomain() was called.

Both fixed in one PR. Translators on weglot, polyglots, and direct community contributors now have the full string surface to work with.

v4.1.58, Anchor tags double-escaped + wp_kses preservation#

Two related bugs in cell rendering:

Frontend: renderRows() in frontend.js was reassigning displayValue mid-flow, so by the time the value reached escapeHtml(), it might already contain <a> tags from a server-side renderer. Result: visible &lt;a href=&gt; text in the cell instead of a working link. The fix introduces a dedicated cellHtml variable, so escaping only ever runs on raw text values, never on already-rendered HTML.

Backend: sanitize_field_value() in class-gt-ajax.php was always falling through to sanitize_text_field() regardless of input, which strips all HTML. So a user editing a cell containing Click <a href="/x">here</a> would save it as Click here, silently losing the link. The fix detects < in the input and routes through wp_kses() with an allowed-tags list (<a>, <br>, <strong>, <em>).

Together these fixes mean: cells with anchor tags render correctly, edit correctly, and save correctly. Before, all three were broken in subtly different ways.

v4.1.57, Two-tier cache eliminates duplicate DB hits#

The GT_Admin::get_table() function (the table-config lookup) ran on every render. On a page with three Gravity Tables shortcodes, that meant three identical DB queries per page load.

The fix introduces a two-tier cache:

  1. Request-level: a PHP static $cache variable. The first lookup queries the DB; the next 2+ on the same page get the cached result for free.
  2. Cross-request: wp_cache_get() / wp_cache_set() against the persistent object cache (Redis / Memcached). The table config persists across requests until invalidated.

A second sub-fix in the same release: checkTableAccessPermission() was issuing its own redundant $wpdb->get_row(), even though the calling context (render_table()) had already loaded the same table config. The fix removes the redundant query.

Net effect: a page with 5 instances of the same table dropped from 5 cold DB queries to 1 cold DB query + 4 cached hits. On Redis-backed sites, the request-level + cross-request layering means even the cold query happens at most once per cache window.

This is documented in more detail in the performance doc.

v4.1.56, Bulk delete capability check correctness#

The bulk-delete action was checking only the generic WordPress edit_posts capability, which any contributor-and-up role has. But Gravity Forms ships its own gform_full_access and gravityforms_delete_entries capabilities for fine-grained entry-deletion control.

Before the fix: a user with edit_posts but without GF’s delete capability could trigger bulk delete and either pass silently (deleting the entries) or hit a generic WP capability error (which doesn’t explain the actual missing permission).

After: the bulk-delete action checks both the WP base capability AND the GF-specific delete capability. Users without GF’s delete cap get a proper JSON error explaining which permission is missing.

A second front-end sub-fix in the same release: performBulkAction in frontend.js now explicitly unchecks all .gt-entry-checkbox inputs immediately after a successful bulk action, before the AJAX reload. Prevents stale selections from persisting if the browser holds previous state.

v4.1.55, TinyMCE classic-editor integration + smart-quote prevention#

For sites still using the Classic editor (TinyMCE) instead of Gutenberg, two pieces shipped:

Toolbar button: A new assets/js/gt-tinymce.js plugin registers an “Insert Gravity Table” button on the TinyMCE toolbar. Click it, get a dialog asking for table id, paste [gravity_table id="X"] into the editor at the cursor.

Smart-quote prevention: TinyMCE’s “Visual” mode applies the WordPress wptexturize filter, which converts straight quotes (") to curly quotes (" / ") in body text. Normally that’s correct. For shortcodes, it breaks them, [gravity_table id="42"] becomes [gravity_table id="42"] and the WP shortcode parser fails to match.

The fix: registers gravity_table and gravity_tables with the no_texturize_shortcodes filter, which excludes their attribute strings from texturisation. Visual-mode editing stays smart-quote-friendly for prose, leaves shortcode attributes alone.

v4.1.54, jQuery event delegation + handler accumulation guard#

A subtle bug that only manifested on rich pages where the table re-bound its event listeners (e.g., after an AJAX page change, or when another plugin triggered a DOM mutation). Each rebind would add a new click listener for pagination prev/next, never removing the old ones, so by the third rebind, clicking “next” would advance the page three times.

The fix:

// Before
$wrapper.find('.gt-prev-page').on('click', handler);

// After
$wrapper.on('click.gt-table', '.gt-prev-page', handler);

Two improvements in one line:

  • Event delegation ($wrapper.on(..., '.selector', handler)) means the handler stays attached to the wrapper, not to specific child elements. Pagination HTML can regenerate without orphaning handlers.
  • Namespaced with .gt-table, so the $wrapper.off('.gt-table') call at the top of bindEvents() cleans up exactly our handlers without touching listeners other plugins (Site Reviews, WP-PageNavi, WooCommerce) might have attached.

The fix audit verified: no Gravity Tables click handler uses document-level or generic selectors that could cross-fire with third-party plugins.

v4.1.53, CSS cascade ordering for per-table custom CSS#

Per-table custom CSS is generated server-side from the table’s typography/style settings (the v4.1.64 typography service builds on this). Before the fix, the <style> block was emitted before the table’s opening <div>, which means the per-table CSS appeared before the host theme’s stylesheets in the document, and lost the cascade race.

Result: setting header_color: '#0066cc' in a table’s typography config would be overridden by any theme .gt-table thead { color: ... } rule loaded later.

The fix is one-line: emit the <style> block after the closing wrapper </div> instead of before the opening <div>. Now the per-table CSS appears later in the document than header-loaded stylesheets and wins the cascade naturally, no !important needed.

v4.1.52, Window.resize handler namespacing#

Same problem as v4.1.54, different surface. Two $(window).on('resize', ...) handlers in frontend.js (one for scroll indicators, one for sticky headers) were attached without namespaces. Multiple tables on the same page meant multiple handlers stacking up; any rebind left orphans behind.

The fix: namespace each handler with the table’s instance id:

// Before
$(window).on('resize', setupScrollIndicators);

// After
$(window).on('resize.gtScroll.' + instanceId, setupScrollIndicators);

Now $(window).off('resize.gtScroll.' + instanceId) cleans up exactly the right handler when a table unmounts.

v4.1.51, CSP-strict via wp_add_inline_script#

For sites that ship a strict Content Security Policy header, common on hardened WP installs (school districts, government, healthcare), raw <script> blocks emitted by plugins fail the CSP check unless they carry a nonce. Two paths in Gravity Tables were emitting bare <script> blocks:

  1. The fallback path in templates/table.php
  2. The map-iframe handler in class-gt-map.php
  3. The export-iframe handler in class-gt-ajax.php (used echo '<script>' literal pattern)

The fix replaces all three with wp_add_inline_script() calls. WP’s helper handles nonce-tagging automatically; the resulting <script nonce="..."> passes a CSP-strict policy without unsafe-eval or unsafe-inline.

Existing shortcodes and map embeds are unaffected, the rendered HTML is identical except for the nonce attribute, which CSP-strict sites will now honor.

What this batch reveals#

Two patterns to notice across these nine fixes:

  1. Multi-instance bugs cluster. Five of the nine (4.1.51 inline-script, 4.1.52 resize handlers, 4.1.54 pagination handlers, 4.1.57 redundant queries, 4.1.58 cell-render escape ordering) only manifest when two or more tables exist on the same page. That’s a single test path that catches multiple bug classes, and one we test against on every release now.

  2. WordPress plugin compat hides real bugs. TinyMCE smart-quote (4.1.55), CSP-strict (4.1.51), object cache invalidation (covered in 4.1.25, related to 4.1.57), Gravity Forms capability layering (4.1.56), wptexturize attribute mangling (4.1.55), all bugs that surface only when another component does its job correctly. None show up in isolated unit tests.

The takeaway for production-stack reviewers: the plugin is being maintained at a level where customer-reported edge cases get fixes in the next nightly, not “we’ll roll that into the 4.3 line.”

Upgrade path#

Every fix in this batch is additive. No setting changes, no deprecation warnings, no migration. If you’ve been holding off upgrading because of a specific symptom listed above, this is the version that resolves it.

  • Performance doc, the two-tier cache pattern from 4.1.57 is documented in full
  • Security page, the CSP-strict + capability-correctness work fits the broader trust posture
  • Embed and customize sprint, what came next, the “after” to this batch’s “before-the-feature-wave”
#stability#i18n#caching#tinymce#csp#roundup