Skip to content

Fix: Support multiple shortcode instances on the same page and add filtering capabilities - #2

Open
pattonwebz wants to merge 8 commits into
mainfrom
william/9675916490/allow-multiple-shortcodes-on-same-page-to-work
Open

Fix: Support multiple shortcode instances on the same page and add filtering capabilities#2
pattonwebz wants to merge 8 commits into
mainfrom
william/9675916490/allow-multiple-shortcodes-on-same-page-to-work

Conversation

@pattonwebz

@pattonwebz pattonwebz commented Mar 17, 2026

Copy link
Copy Markdown
Member

Adds tags to the meeting minutes cpt, adds some tag filtering capabilities to the shortcode as well as having optional output of said tags (also with clickable filters).

Additionally it was made to be able to have multiple on a page at once. Details about that breaking change is below:

Previously, the [edmm_meeting_minutes] shortcode used hard-coded element IDs (edmm-meeting-minutes-table, edmm-pagination, pagination-info), meaning only the first instance on a page would render correctly. Additional instances would silently update the first instance's DOM instead of their own.

This change generates a unique ID per shortcode instance (e.g., edmm-instance-1-table) and scopes all JavaScript DOM queries to that instance.

Possible breaking changes

The following IDs no longer exist in the rendered HTML and any CSS or JavaScript targeting them will stop working:

  • #edmm-meeting-minutes-table
  • #edmm-pagination
  • #pagination-info

These have been replaced with classes of the same names so CSS can be migrated easily:

  • .edmm-meeting-minutes-table-wrapper
  • .edmm-pagination
  • .edmm-pagination-info

Anywhere that was styling this by the ids will need to be updated for this.

Summary by CodeRabbit

  • New Features

    • Added per-meeting categories and tags; both are included on meeting records and filterable via API parameters.
    • Optional categories column, tag/category filter controls (buttons), and per-instance UID-scoped UI with keyboard-friendly interactions.
  • Bug Fixes

    • Multiple shortcode instances run independently without ID collisions.
    • Pagination and filtering respect per-instance state and propagate filters through the API.
  • Documentation

    • Updated docs for tags, categories, shortcode attributes, and REST filtering.

…multiple to be on the same page

Possible breaking changes

The following IDs no longer exist in the rendered HTML and any CSS or JavaScript targeting them will stop working:

  - #edmm-meeting-minutes-table
  - #edmm-pagination
  - #pagination-info
These have been replaced with classes of the same names so CSS can be migrated easily:

  - .edmm-meeting-minutes-table-wrapper
  - .edmm-pagination
  - .edmm-pagination-info
Copilot AI review requested due to automatic review settings March 17, 2026 20:13
@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new per-meeting taxonomy edmm_meeting_category and registers it. Extends the REST endpoint to accept category_filter and tags parameters and include per-meeting category data in responses. Updates the shortcode output and frontend JS/CSS to use UID-scoped DOM IDs, render category-filter UI and CSS, propagate filters through pagination, and expose category data in the table. Documents tag-related shortcode attributes and REST parameter.

Changes

Cohort / File(s) Summary
Core plugin
equalize-digital-meeting-minutes.php
Registers edmm_meeting_category taxonomy and adds register_meeting_minutes_taxonomy() + init hook. Extends REST handling to accept category_filter and tags, builds a tax_query for edmm_meeting_category, includes per-meeting category data (name, slug, parent) in API responses, replaces static DOM IDs with UID-prefixed IDs for multi-instance shortcodes, adds category-filter UI and CSS, and ensures filters (category & tags) propagate through shortcode API URLs and pagination.
Documentation
README.md
Adds documentation for Meeting Tags and Categories (edmm_meeting_tag, edmm_meeting_category), introduces tags, hide_tags, and tags_label shortcode attributes, and documents the tags and category_filter REST parameters and shortcode filtering behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Browser as Shortcode UI
  participant Server as REST Endpoint
  participant WP as WordPress DB

  User->>Browser: Click category filter (UID-scoped)
  Browser->>Server: GET /wp-json/.../meeting-minutes?category_filter=slug&tags=...
  Server->>WP: Build WP_Query (tax_query for edmm_meeting_category + tags)
  WP-->>Server: Query results with category data attached
  Server-->>Browser: JSON response including categories per meeting
  Browser->>Browser: Render table/pagination using UID-scoped IDs
  Browser-->>User: Updated meeting list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through taxonomies with glee,
UID hats kept elements free,
Categories lined in tidy rows,
Tags danced where the REST call goes,
A small rabbit's joyful decree.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding support for multiple shortcode instances on the same page and implementing filtering capabilities (tags/categories).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch william/9675916490/allow-multiple-shortcodes-on-same-page-to-work
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where the [edmm_meeting_minutes] shortcode failed to render correctly when used multiple times on the same page. By introducing a unique identifier for each shortcode instance and updating the associated JavaScript to scope its DOM manipulations to these unique IDs, the shortcode now functions independently and reliably across all instances on a page. This significantly improves the flexibility and usability of the shortcode for content creators.

Highlights

  • Support for Multiple Shortcode Instances: Implemented a mechanism to generate unique IDs for each instance of the [edmm_meeting_minutes] shortcode, ensuring proper rendering when multiple instances are present on a single page.
  • Scoped JavaScript DOM Queries: Updated all JavaScript DOM queries to target elements using their newly generated unique IDs, preventing conflicts and ensuring correct functionality across multiple shortcode instances.
  • ID to Class Migration for Styling: Replaced hardcoded element IDs (#edmm-meeting-minutes-table, #edmm-pagination, #pagination-info) with dynamically generated IDs and introduced corresponding classes (.edmm-meeting-minutes-table-wrapper, .edmm-pagination, .edmm-pagination-info) to facilitate easier CSS and JS migration for styling.
Changelog
  • equalize-digital-meeting-minutes.php
    • Introduced a static counter and unique ID generation for each shortcode instance.
    • Dynamically generated IDs for the main table wrapper, pagination, and pagination info div elements.
    • Added new CSS classes to the dynamically generated div elements for styling purposes.
    • Updated all JavaScript document.getElementById and document.querySelectorAll calls to use the new unique instance IDs.
Activity
  • No activity has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request modifies the meeting_minutes_shortcode function to support multiple instances on a single page by generating unique IDs for the HTML elements (table, pagination, and info divs) and updating the corresponding JavaScript to reference these dynamic IDs, thereby preventing ID collisions. The reviewer suggests an improvement to replace an inline style used for visually hiding an element with the standard WordPress .screen-reader-text utility class for better maintainability and consistency.

<div id="pagination-info" aria-live="polite" aria-atomic="true" style="position: absolute; left: -9999px;"></div>
<div id="<?php echo esc_attr( $uid ); ?>-table" class="edmm-meeting-minutes-table-wrapper"></div>
<div id="<?php echo esc_attr( $uid ); ?>-pagination" class="edmm-pagination"></div>
<div id="<?php echo esc_attr( $uid ); ?>-info" class="edmm-pagination-info" aria-live="polite" aria-atomic="true" style="position: absolute; left: -9999px;"></div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better maintainability and adherence to WordPress standards, it's recommended to use a utility class for visually hiding elements instead of an inline style. WordPress core provides the .screen-reader-text class for this purpose, which you're already using elsewhere. Using this class centralizes the styling logic and makes the code cleaner.

    <div id="<?php echo esc_attr( $uid ); ?>-info" class="edmm-pagination-info screen-reader-text" aria-live="polite" aria-atomic="true"></div>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the [edmm_meeting_minutes] shortcode output so multiple instances can coexist on the same page without clobbering each other’s DOM, by generating per-instance unique element IDs and scoping DOM lookups accordingly.

Changes:

  • Generate a unique $uid per shortcode render and use it to suffix container element IDs.
  • Update the inline JavaScript to target instance-specific IDs for table rendering, pagination rendering, aria-live updates, and focus management.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pattonwebz pattonwebz changed the title Fix: Support multiple shortcode instances on the same page Fix: Support multiple shortcode instances on the same page and add filtering capabilities Mar 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
equalize-digital-meeting-minutes.php (1)

420-420: ⚠️ Potential issue | 🟡 Minor

Missing space before aria-current attribute produces invalid HTML.

The string concatenation creates aria-label="Page 1"aria-current="true" without a space between attributes, which is invalid HTML and may break screen reader interpretation.

🐛 Proposed fix
-                            pagination += '<button type="button" class="edmm-pagination-button' + (slot === data.current_page ? ' current' : '') + '" data-page="' + slot + '" aria-label="Page ' + slot + '"'+ (slot === data.current_page ? 'aria-current="true"' : '') +'>' + slot + '</button>';
+                            pagination += '<button type="button" class="edmm-pagination-button' + (slot === data.current_page ? ' current' : '') + '" data-page="' + slot + '" aria-label="Page ' + slot + '"' + (slot === data.current_page ? ' aria-current="true"' : '') + '>' + slot + '</button>';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` at line 420, The pagination button
string concatenation misses a space before the aria-current attribute, producing
invalid HTML; update the concatenation that builds the pagination string (the
line that appends to the pagination variable using slot and data.current_page)
so the ternary that outputs aria-current includes a leading space when true
(e.g., change the ternary to return ' aria-current="true"' instead of
'aria-current="true"'), ensuring no extra space is added when false.
🧹 Nitpick comments (3)
equalize-digital-meeting-minutes.php (3)

210-230: Consider documenting the distinction between tags and tags_filter parameters.

Both parameters filter by the same taxonomy with an AND relation, which could confuse maintainers. From the shortcode usage: tags_filter is the permanent shortcode-level filter while tags is the dynamic user-selected filter. A brief inline comment would clarify this intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 210 - 230, Add a brief
inline comment above the $tax_query handling explaining the difference between
the two parameters: $tags_filter is the permanent shortcode-level filter and
$tags is the dynamic user-selected filter, both targeting the 'edmm_meeting_tag'
taxonomy and combined with an AND relation; place this comment close to where
$tags_filter and $tags are checked (the blocks that append to $tax_query and the
final count check that sets $args['tax_query']) so future maintainers understand
the intent.

379-388: Consider escaping tag data when building HTML strings.

While taxonomy term names come from WordPress admin input (trusted), building HTML via string concatenation without escaping is a fragile pattern. If term names ever contain special characters like < or ", the markup could break.

♻️ Defensive escaping approach

Consider using a simple escape helper or the DOM API:

// Helper at the top of the script
const escapeHtml = (str) => str.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);

// Then in the loop:
tagButtons += '<button type="button" class="edmm-tag-filter' + (isActive ? ' active' : '') + '" data-slug="' + escapeHtml(tag.slug) + '" aria-pressed="' + isActive + '">' + escapeHtml(tag.name) + '</button>';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 379 - 388, The current
string-concatenation that builds tagButtons using meeting.tags (inside the block
that checks '<?php echo esc_js( $atts['hide_tags'] ); ?>') is not escaping
tag.slug or tag.name and can break markup if they contain special characters;
add a small escape helper (e.g., escapeHtml) near the top of the script and use
it when inserting tag.slug and tag.name into the button string (keep using
activeTagFilter and the existing isActive logic), or build the button via DOM
APIs to avoid direct string injection.

333-608: Consider extracting inline scripts/styles to enqueued assets for larger deployments.

The current inline approach works correctly and maintains instance isolation via closures. However, if this plugin scales to pages with many shortcode instances, the duplicated CSS and common JavaScript functions (like calculatePaginationSlots) could be extracted to an enqueued file with instance-specific data passed via wp_localize_script(). This is a future optimization rather than a current issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 333 - 608, The inline JS
and CSS (including functions like calculatePaginationSlots, renderTable,
fetchMeetings and constants maxSlots, postsPerPage, apiUrl, uid, and the CSS for
.edmm-meeting-minutes-table/.edmm-pagination-button/.edmm-tag-filter) should be
extracted into enqueued assets; move common JS to a single bundled script and
common CSS to a stylesheet, then pass instance-specific values (uid, apiUrl,
postsPerPage, activeTagFilter, any att flags) into that script via
wp_localize_script or wp_add_inline_script so the bundled
calculatePaginationSlots and event handlers operate per-instance using the
localized uid/selectors and avoid duplicating styles and functions across
multiple shortcode instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@equalize-digital-meeting-minutes.php`:
- Line 420: The pagination button string concatenation misses a space before the
aria-current attribute, producing invalid HTML; update the concatenation that
builds the pagination string (the line that appends to the pagination variable
using slot and data.current_page) so the ternary that outputs aria-current
includes a leading space when true (e.g., change the ternary to return '
aria-current="true"' instead of 'aria-current="true"'), ensuring no extra space
is added when false.

---

Nitpick comments:
In `@equalize-digital-meeting-minutes.php`:
- Around line 210-230: Add a brief inline comment above the $tax_query handling
explaining the difference between the two parameters: $tags_filter is the
permanent shortcode-level filter and $tags is the dynamic user-selected filter,
both targeting the 'edmm_meeting_tag' taxonomy and combined with an AND
relation; place this comment close to where $tags_filter and $tags are checked
(the blocks that append to $tax_query and the final count check that sets
$args['tax_query']) so future maintainers understand the intent.
- Around line 379-388: The current string-concatenation that builds tagButtons
using meeting.tags (inside the block that checks '<?php echo esc_js(
$atts['hide_tags'] ); ?>') is not escaping tag.slug or tag.name and can break
markup if they contain special characters; add a small escape helper (e.g.,
escapeHtml) near the top of the script and use it when inserting tag.slug and
tag.name into the button string (keep using activeTagFilter and the existing
isActive logic), or build the button via DOM APIs to avoid direct string
injection.
- Around line 333-608: The inline JS and CSS (including functions like
calculatePaginationSlots, renderTable, fetchMeetings and constants maxSlots,
postsPerPage, apiUrl, uid, and the CSS for
.edmm-meeting-minutes-table/.edmm-pagination-button/.edmm-tag-filter) should be
extracted into enqueued assets; move common JS to a single bundled script and
common CSS to a stylesheet, then pass instance-specific values (uid, apiUrl,
postsPerPage, activeTagFilter, any att flags) into that script via
wp_localize_script or wp_add_inline_script so the bundled
calculatePaginationSlots and event handlers operate per-instance using the
localized uid/selectors and avoid duplicating styles and functions across
multiple shortcode instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2e79c88-ae09-4b21-a524-40eedab843e6

📥 Commits

Reviewing files that changed from the base of the PR and between 87c80d8 and 849768c.

📒 Files selected for processing (2)
  • README.md
  • equalize-digital-meeting-minutes.php
✅ Files skipped from review due to trivial changes (1)
  • README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@equalize-digital-meeting-minutes.php`:
- Around line 262-267: The category values (term->name and term->slug) stored
into $categories_data are later injected into the DOM via innerHTML and must be
escaped to prevent stored XSS; update the code that populates or outputs
$categories_data so the values are safely encoded (e.g., escape when building
the PHP array with esc_html/esc_attr or better, output data via wp_json_encode
and in the JS assign into the DOM using textContent/createTextNode or properly
escape before setting innerHTML). Ensure you update the handling around
$category_terms / $categories_data and any JS that uses category.name or
category.slug to use the safe-encoding approach rather than concatenating raw
values into innerHTML.
- Around line 336-337: activeCategoryFilter is initialized from
esc_js($atts['tags']) while later interactive fetches append a 'tags' query
param; unify both to use 'category_filter' on the client side. Change the
initialization of activeCategoryFilter to read the server-provided
category_filter value (falling back to tags only on server-side PHP if needed)
and update the interactive fetch code that currently appends 'tags' to instead
append 'category_filter', leaving any server-side handling of 'tags' as a
backward-compatible alias.
- Around line 309-311: The default user-facing string assigned to
'categories_label' is not localized; update the value for 'categories_label' to
use a translation function with the plugin text domain (e.g., __('Categories',
'your-plugin-text-domain') or esc_html__('Categories',
'your-plugin-text-domain')) so the default label can be translated; locate the
array entry for 'categories_label' and replace the raw "Categories" string
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13570afb-d23b-452f-8248-ea83c4616035

📥 Commits

Reviewing files that changed from the base of the PR and between 849768c and 430659b.

📒 Files selected for processing (1)
  • equalize-digital-meeting-minutes.php

Comment on lines +262 to +267
$category_terms = get_the_terms( get_the_ID(), 'edmm_meeting_category' );
$categories_data = array();
if ( $category_terms && ! is_wp_error( $category_terms ) ) {
foreach ( $category_terms as $term ) {
$categories_data[] = array( 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape category values before injecting into innerHTML.

On Line 384 and Line 387, category.slug/category.name are concatenated directly into HTML. These values should be escaped before insertion to avoid stored-XSS risk via taxonomy data.

🔒 Suggested hardening
             const renderTable = (data, refocus = false) => {
                 refocus = refocus || false;
+                const escapeHtml = (value) =>
+                    String(value).replace(/[&<>"']/g, (ch) => ({
+                        '&': '&amp;',
+                        '<': '&lt;',
+                        '>': '&gt;',
+                        '"': '&quot;',
+                        "'": '&#39;',
+                    }[ch]));

                 let tableClass = '<?php echo esc_js( $atts['class'] ); ?>';
@@
                         if (meeting.categories && meeting.categories.length) {
                             meeting.categories.forEach(category => {
-                                const isActive = activeCategoryFilter === category.slug;
-                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + category.slug + '" aria-pressed="' + isActive + '">' + category.name + '</button>';
+                                const rawSlug = String(category.slug || '');
+                                const isActive = activeCategoryFilter === rawSlug;
+                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + escapeHtml(rawSlug) + '" aria-pressed="' + isActive + '">' + escapeHtml(category.name || '') + '</button>';
                             });
                         }

Also applies to: 379-388

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 262 - 267, The category
values (term->name and term->slug) stored into $categories_data are later
injected into the DOM via innerHTML and must be escaped to prevent stored XSS;
update the code that populates or outputs $categories_data so the values are
safely encoded (e.g., escape when building the PHP array with esc_html/esc_attr
or better, output data via wp_json_encode and in the JS assign into the DOM
using textContent/createTextNode or properly escape before setting innerHTML).
Ensure you update the handling around $category_terms / $categories_data and any
JS that uses category.name or category.slug to use the safe-encoding approach
rather than concatenating raw values into innerHTML.

Comment on lines +309 to +311
'hide_categories' => 'true',
'categories_label' => 'Categories', // only used if hide_categories is false.
'category_filter' => '', // outputs items only with this category slug.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the new default categories label.

Line 310 introduces a user-facing default string ('Categories') without translation wrapping. Use the plugin text domain so this default can be localized.

🌐 Suggested change
-        'categories_label'     => 'Categories', // only used if hide_categories is false.
+        'categories_label'     => __( 'Categories', 'edmm' ), // only used if hide_categories is false.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'hide_categories' => 'true',
'categories_label' => 'Categories', // only used if hide_categories is false.
'category_filter' => '', // outputs items only with this category slug.
'hide_categories' => 'true',
'categories_label' => __( 'Categories', 'edmm' ), // only used if hide_categories is false.
'category_filter' => '', // outputs items only with this category slug.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 309 - 311, The default
user-facing string assigned to 'categories_label' is not localized; update the
value for 'categories_label' to use a translation function with the plugin text
domain (e.g., __('Categories', 'your-plugin-text-domain') or
esc_html__('Categories', 'your-plugin-text-domain')) so the default label can be
translated; locate the array entry for 'categories_label' and replace the raw
"Categories" string accordingly.

Comment thread equalize-digital-meeting-minutes.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
equalize-digital-meeting-minutes.php (3)

169-173: ⚠️ Potential issue | 🟠 Major

tags alias handling is inconsistent and can break expected filtering.

Line 304 still exposes tags, but the shortcode bootstrap now only seeds category_filter (Line 336) and the REST layer can combine both via AND (Line 210-230). This can break legacy shortcode usage and produce overly restrictive results when both are present.

🔧 Suggested alignment (single effective filter, `tags` as fallback alias)
 function get_meeting_minutes( $request ) {
@@
-    $tags                 = isset($params['tags']) ? sanitize_text_field($params['tags']) : '';
-	$category_filter      = isset($params['category_filter']) ? sanitize_text_field($params['category_filter']) : '';
+	$category_filter      = isset($params['category_filter']) ? sanitize_text_field($params['category_filter']) : '';
+    if ( '' === $category_filter && isset( $params['tags'] ) ) {
+        $category_filter = sanitize_text_field( $params['tags'] );
+    }
@@
-    if ( ! empty( $tags ) ) {
-        $tax_query[] = array(
-            'taxonomy' => 'edmm_meeting_category',
-            'field'    => 'slug',
-            'terms'    => array_map( 'trim', explode( ',', $tags ) ),
-        );
-    }
-
     if ( count( $tax_query ) > 1 ) {
         $args['tax_query'] = $tax_query;
     }
 }
@@
 function meeting_minutes_shortcode( $atts ) {
     $atts = shortcode_atts( array(
@@
     ), $atts, 'edmm_meeting_minutes' );
+    $initial_category_filter = ! empty( $atts['category_filter'] ) ? $atts['category_filter'] : $atts['tags'];
 
     $api_url = rest_url( 'edmm/v1/meeting-minutes/' ) . '?included_years=' . urlencode( $atts['included_years'] )
@@
-        . ( ! empty( $atts['category_filter'] ) ? '&category_filter=' . urlencode( $atts['category_filter'] ) : '' );
+        . ( ! empty( $initial_category_filter ) ? '&category_filter=' . urlencode( $initial_category_filter ) : '' );
@@
-            let activeCategoryFilter = '<?php echo esc_js( $atts['category_filter'] ); ?>';
+            let activeCategoryFilter = '<?php echo esc_js( $initial_category_filter ); ?>';

Also applies to: 210-230, 304-304, 318-323, 336-337

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 169 - 173, The handling of
the tags parameter is inconsistent and can make filtering overly restrictive;
change the bootstrap so tags is treated strictly as a fallback alias for
category_filter: after sanitizing both $tags and $category_filter in the
shortcode/bootstrap (variables $tags and $category_filter), if $category_filter
is empty and $tags is not, assign $category_filter = $tags and thereafter only
use $category_filter for filtering and REST requests; remove or stop exposing
$tags downstream (e.g., where results are seeded or passed to REST) so the REST
layer and shortcode always use a single effective filter (category_filter) and
legacy shortcodes still work.

310-310: ⚠️ Potential issue | 🟡 Minor

Localize the default categories label.

Line 310 introduces a user-facing default string without i18n wrapping.

🌐 Suggested fix
-        'categories_label'     => 'Categories', // only used if hide_categories is false.
+        'categories_label'     => __( 'Categories', 'edmm' ), // only used if hide_categories is false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` at line 310, The default user-facing
string assigned to the 'categories_label' setting is not internationalized;
replace the raw 'Categories' literal with a localization call (e.g.
__('Categories', 'equalize-digital-meeting-minutes') or esc_html__('Categories',
'equalize-digital-meeting-minutes')) so the label is translatable—update the
value for the 'categories_label' key where default settings are defined to use
the chosen i18n function and the plugin text domain.

262-267: ⚠️ Potential issue | 🟠 Major

Escape category values before building HTML strings.

Line 266 passes raw term data through the API, and Line 384 injects it into innerHTML without escaping. This keeps a stored-XSS surface in the category filter UI.

🔒 Suggested hardening
             const renderTable = (data, refocus = false) => {
                 refocus = refocus || false;
+                const escapeHtml = (value) =>
+                    String(value).replace(/[&<>"']/g, (ch) => ({
+                        '&': '&amp;',
+                        '<': '&lt;',
+                        '>': '&gt;',
+                        '"': '&quot;',
+                        "'": '&#39;',
+                    }[ch]));
@@
                         if (meeting.categories && meeting.categories.length) {
                             meeting.categories.forEach(category => {
-                                const isActive = activeCategoryFilter === category.slug;
-                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + category.slug + '" aria-pressed="' + isActive + '">' + category.name + '</button>';
+                                const rawSlug = String(category.slug || '');
+                                const isActive = activeCategoryFilter === rawSlug;
+                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + escapeHtml(rawSlug) + '" aria-pressed="' + isActive + '">' + escapeHtml(category.name || '') + '</button>';
                             });
                         }

Also applies to: 379-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 262 - 267, The category
term values are added raw into $categories_data (variable built from
$category_terms) and later injected into the page via innerHTML (around the code
that renders the category filter), creating a stored-XSS risk; sanitize all term
fields before output by escaping when building the HTML/JS: use esc_html() or
esc_attr() for name/slug/parent when embedding into HTML attributes or content,
or use wp_json_encode()/esc_js() when passing the array into inline JS, and
ensure the code that uses $categories_data for innerHTML uses the
escaped/encoded values instead of raw $term->name/$term->slug.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@equalize-digital-meeting-minutes.php`:
- Around line 547-561: The category filter styles (.edmm-category-filter and
.edmm-category-filter.active) use `#007bff` and white which fail WCAG AA contrast
for normal-size text; replace those colors with combinations that meet 4.5:1
contrast (for example use a darker blue like `#0056b3` for borders/text and white
for active text, or keep `#007bff` but change text to `#111827` for non-active) and
ensure border-color, color and background-color in both .edmm-category-filter
and .edmm-category-filter.active are updated consistently so non-active text and
active text/background meet >=4.5:1 contrast against their backgrounds.

---

Duplicate comments:
In `@equalize-digital-meeting-minutes.php`:
- Around line 169-173: The handling of the tags parameter is inconsistent and
can make filtering overly restrictive; change the bootstrap so tags is treated
strictly as a fallback alias for category_filter: after sanitizing both $tags
and $category_filter in the shortcode/bootstrap (variables $tags and
$category_filter), if $category_filter is empty and $tags is not, assign
$category_filter = $tags and thereafter only use $category_filter for filtering
and REST requests; remove or stop exposing $tags downstream (e.g., where results
are seeded or passed to REST) so the REST layer and shortcode always use a
single effective filter (category_filter) and legacy shortcodes still work.
- Line 310: The default user-facing string assigned to the 'categories_label'
setting is not internationalized; replace the raw 'Categories' literal with a
localization call (e.g. __('Categories', 'equalize-digital-meeting-minutes') or
esc_html__('Categories', 'equalize-digital-meeting-minutes')) so the label is
translatable—update the value for the 'categories_label' key where default
settings are defined to use the chosen i18n function and the plugin text domain.
- Around line 262-267: The category term values are added raw into
$categories_data (variable built from $category_terms) and later injected into
the page via innerHTML (around the code that renders the category filter),
creating a stored-XSS risk; sanitize all term fields before output by escaping
when building the HTML/JS: use esc_html() or esc_attr() for name/slug/parent
when embedding into HTML attributes or content, or use wp_json_encode()/esc_js()
when passing the array into inline JS, and ensure the code that uses
$categories_data for innerHTML uses the escaped/encoded values instead of raw
$term->name/$term->slug.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4311d0fa-fd7b-4993-815a-658b6b208e8c

📥 Commits

Reviewing files that changed from the base of the PR and between 430659b and d0c22e1.

📒 Files selected for processing (1)
  • equalize-digital-meeting-minutes.php

Comment thread equalize-digital-meeting-minutes.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
equalize-digital-meeting-minutes.php (3)

310-310: ⚠️ Potential issue | 🟡 Minor

Localize the default categories_label string.

Line 310 uses a raw user-facing string, so it cannot be translated by default.

🌐 Suggested fix
-        'categories_label'     => 'Categories', // only used if hide_categories is false.
+        'categories_label'     => __( 'Categories', 'edmm' ), // only used if hide_categories is false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` at line 310, Replace the raw
user-facing string for the array key 'categories_label' with a translatable call
(e.g. __('Categories', 'your-textdomain') or _x(...) as appropriate) so the
default label can be localized; update the value for 'categories_label' where it
is defined to use the translation function and use the plugin/theme text domain
used elsewhere in the codebase.

262-267: ⚠️ Potential issue | 🟠 Major

Escape category values before concatenating HTML for innerHTML.

Line 266 and Line 384 still pass raw taxonomy data into HTML strings. A crafted term name/slug can inject markup/script when rendered.

🔒 Suggested hardening
             const renderTable = (data, refocus = false) => {
                 refocus = refocus || false;
+                const escapeHtml = (value) =>
+                    String(value).replace(/[&<>"']/g, (ch) => ({
+                        '&': '&amp;',
+                        '<': '&lt;',
+                        '>': '&gt;',
+                        '"': '&quot;',
+                        "'": '&#39;',
+                    }[ch]));

@@
                     if ('<?php echo esc_js( $atts['hide_categories'] ); ?>' !== 'true') {
                         let categoryButtons = '';
                         if (meeting.categories && meeting.categories.length) {
                             meeting.categories.forEach(category => {
-                                const isActive = activeCategoryFilter === category.slug;
-                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + category.slug + '" aria-pressed="' + isActive + '">' + category.name + '</button>';
+                                const rawSlug = String(category.slug || '');
+                                const rawName = String(category.name || '');
+                                const isActive = activeCategoryFilter === rawSlug;
+                                categoryButtons += '<button type="button" class="edmm-category-filter' + (isActive ? ' active' : '') + '" data-slug="' + escapeHtml(rawSlug) + '" aria-pressed="' + isActive + '">' + escapeHtml(rawName) + '</button>';
                             });
                         }
                         table += '<td data-label="<?php echo esc_js( $atts['categories_label'] ); ?>">' + categoryButtons + '</td>';
                     }

Also applies to: 379-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 262 - 267, The code
currently pushes raw term values from $category_terms into $categories_data and
later concatenates them into innerHTML (term->name, term->slug), allowing XSS;
sanitize/encode these values before output: either sanitize when building
$categories_data (e.g., use esc_html( $term->name ) / esc_attr( $term->slug ) or
use wp_json_encode() / esc_js() when passing data into JS) and ensure any string
concatenation assigned to innerHTML uses those escaped values; update the code
paths that build $categories_data and where innerHTML is set (references:
$category_terms, $categories_data, $term->name, $term->slug, innerHTML) so all
taxonomy values are escaped for the correct context.

171-173: ⚠️ Potential issue | 🟠 Major

tags shortcode compatibility is broken and alias handling can over-filter.

tags is still a declared shortcode attribute (Line 304), but the client only initializes/sends category_filter (Line 322, Line 336). Also, server logic adds both params into tax_query with AND (Line 220-Line 230), which can unintentionally narrow results when both are present. Treat them as aliases and resolve one effective filter.

🧭 Suggested alignment
 function get_meeting_minutes( $request ) {
@@
-    $tags                 = isset($params['tags']) ? sanitize_text_field($params['tags']) : '';
-	$category_filter      = isset($params['category_filter']) ? sanitize_text_field($params['category_filter']) : '';
+    $tags                 = isset($params['tags']) ? sanitize_text_field($params['tags']) : '';
+	$category_filter      = isset($params['category_filter']) ? sanitize_text_field($params['category_filter']) : '';
+    $effective_filter     = $category_filter ?: $tags;
@@
-    if ( ! empty( $category_filter ) ) {
-        $tax_query[] = array(
-            'taxonomy' => 'edmm_meeting_category',
-            'field'    => 'slug',
-            'terms'    => array_map( 'trim', explode( ',', $category_filter ) ),
-        );
-    }
-
-    if ( ! empty( $tags ) ) {
+    if ( ! empty( $effective_filter ) ) {
         $tax_query[] = array(
             'taxonomy' => 'edmm_meeting_category',
             'field'    => 'slug',
-            'terms'    => array_map( 'trim', explode( ',', $tags ) ),
+            'terms'    => array_map( 'trim', explode( ',', $effective_filter ) ),
         );
     }
@@
 function meeting_minutes_shortcode( $atts ) {
@@
+    $initial_filter = ! empty( $atts['category_filter'] ) ? $atts['category_filter'] : $atts['tags'];
+
     $api_url = rest_url( 'edmm/v1/meeting-minutes/' ) . '?included_years=' . urlencode( $atts['included_years'] )
@@
-        . ( ! empty( $atts['category_filter'] ) ? '&category_filter=' . urlencode( $atts['category_filter'] ) : '' );
+        . ( ! empty( $initial_filter ) ? '&category_filter=' . urlencode( $initial_filter ) : '' );
@@
-            let activeCategoryFilter = '<?php echo esc_js( $atts['category_filter'] ); ?>';
+            let activeCategoryFilter = '<?php echo esc_js( $initial_filter ); ?>';

Also applies to: 220-230, 304-305, 318-323, 336-337

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@equalize-digital-meeting-minutes.php` around lines 171 - 173, The
shortcode/server mismatch and over-filtering is caused by treating both $tags
and $category_filter as separate tax_query clauses; change the code that reads
shortcode attributes ($tags and $category_filter via
sanitize_text_field($params['tags'] / $params['category_filter'])) to resolve
them as aliases into a single effective filter variable (e.g.,
$effective_category_filter = $category_filter ?: $tags), ensure the client sends
that single field (so the client-side should initialize/send category_filter
when tags is used), and update the tax_query construction (where tax_query
entries are built) to add only one taxonomy clause using the resolved
$effective_category_filter instead of adding both entries with 'relation' =>
'AND' which over-narrows results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@equalize-digital-meeting-minutes.php`:
- Line 310: Replace the raw user-facing string for the array key
'categories_label' with a translatable call (e.g. __('Categories',
'your-textdomain') or _x(...) as appropriate) so the default label can be
localized; update the value for 'categories_label' where it is defined to use
the translation function and use the plugin/theme text domain used elsewhere in
the codebase.
- Around line 262-267: The code currently pushes raw term values from
$category_terms into $categories_data and later concatenates them into innerHTML
(term->name, term->slug), allowing XSS; sanitize/encode these values before
output: either sanitize when building $categories_data (e.g., use esc_html(
$term->name ) / esc_attr( $term->slug ) or use wp_json_encode() / esc_js() when
passing data into JS) and ensure any string concatenation assigned to innerHTML
uses those escaped values; update the code paths that build $categories_data and
where innerHTML is set (references: $category_terms, $categories_data,
$term->name, $term->slug, innerHTML) so all taxonomy values are escaped for the
correct context.
- Around line 171-173: The shortcode/server mismatch and over-filtering is
caused by treating both $tags and $category_filter as separate tax_query
clauses; change the code that reads shortcode attributes ($tags and
$category_filter via sanitize_text_field($params['tags'] /
$params['category_filter'])) to resolve them as aliases into a single effective
filter variable (e.g., $effective_category_filter = $category_filter ?: $tags),
ensure the client sends that single field (so the client-side should
initialize/send category_filter when tags is used), and update the tax_query
construction (where tax_query entries are built) to add only one taxonomy clause
using the resolved $effective_category_filter instead of adding both entries
with 'relation' => 'AND' which over-narrows results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 382c4805-bc10-4077-b952-b85b89987b67

📥 Commits

Reviewing files that changed from the base of the PR and between d0c22e1 and 3d44747.

📒 Files selected for processing (1)
  • equalize-digital-meeting-minutes.php

@SteveJonesDev

Copy link
Copy Markdown
Member

@pattonwebz, does this need reviewed or is it waiting for something else?

Copy link
Copy Markdown
Member Author

@SteveJonesDev we talked about this in the weekly meeting and it needs to be tweaked to prevent loss of focus when the filtering runs. We decided it was ok to make that change next week and Amber updated the due date on the basecamp ticket.

@pattonwebz

Copy link
Copy Markdown
Member Author

This work was done for a client and is not compatible with the rewrite - but the rewrite does already solve for this exact same set of problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants