You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make a plan for a new Gutenberg block that lists sponsors. First, the editor will be able to specify a list of sponsors in a Google Sheet with the following columns:
- Sponsor Name - free text field
- Post ID - number corresponds with post ID for the corresponding Sponsor CPT post ID
- Level - free text field
- Description - free text field
- Categories - comma-delimited list of categories
- Booth Location - free text field
There may be many (up to 100) sponsors added to a particular block from the Google Sheet. The sheet will be published publicly and the url of the CSV provided to the block to import. To keep things simple, the data does not need to be editable within the Gutenberg editor. If edits need to happen, they’ll happen on the Sheet and the import button pressed again. Sponsors from the sheet will be matched to a corresponding Sponsor CPT post by ID and then the logo and url from that post is used for the front-end display. Skip unmatched rows and report them.
For the actual listing on the front-end of the site, we want it to be filterable by category and level. The listing should show sponsor name, logo, level, booth location, and categories. The list is sorted alphabetically by sponsor name. Each sponsor can be clicked to display a popup with the full information about the sponsor including their website url.
Using backslash as fgetcsv's escape character can corrupt valid Google Sheets CSV data—for example, a quoted free-text field containing a comma and ending in a backslash may consume the closing quote and following columns. Disable the proprietary escape mechanism for row parsing.
This client-side update immediately replaces the localized PHP result count with hard-coded English and cannot support languages with different plural rules. Generate the label with @wordpress/i18n (_n plus sprintf) and rebuild the view bundle.
When an import succeeds but every row is invalid or unmatched, sponsors.length is zero and this branch hides ImportReport, so none of the required skipped-row reasons are shown. Render the report in the empty state as well.
The importer appends every matching row without enforcing the 100-sponsor limit, while lf_sponsor_directory_normalize_sponsors() later silently slices the stored attributes to 100. A 101-row import therefore reports and previews 101 sponsors but renders only 100. Enforce a shared limit during import and explicitly reject or report excess rows rather than silently dropping them at render time.
Google Sheets emits RFC 4180-style CSV, but enabling PHP's proprietary backslash escape can misparse a quoted header whose closing quote is preceded by a literal backslash. Disable the escape character so headers are parsed according to the CSV format.
This issue also appears on line 227 of the same file.
The IDs-only query does not prime post metadata, and the following loop calls has_post_thumbnail() and get_post_meta() for every matched sponsor. This creates an N+1 postmeta query pattern (up to 100 extra queries under the intended limit, and more while the import is uncapped). Prime the matched posts' metadata before iterating.
edit.js directly imports @wordpress/api-fetch, but this package does not declare it and currently relies on @wordpress/scripts installing/hoisting it transitively. A clean install can stop resolving the import when that transitive graph changes. Add @wordpress/api-fetch as a direct development dependency and update the lockfile.
update() immediately replaces the server-rendered, translated plural string with hard-coded English, and languages with plural forms beyond singular/plural cannot translate the result count after filtering. Build this text with @wordpress/i18n (_n plus sprintf) and regenerate the tracked view bundle.
The aria-label does not reliably name this filter set because a plain div has the generic role, which cannot be named. Give the container role="group" (or use a fieldset with a legend) so assistive technology exposes “Filter sponsors” as the controls' group label.
When an import matches zero sponsors—such as when every Post ID is unmatched—this branch hides ImportReport, so the editor cannot see the skipped rows or reasons the import endpoint returned. Include the report when skippedCount is nonzero (or render it independently of the sponsor preview).
- Add aria-label to sponsor card button for better screen reader support
- Remove redundant sponsor name display in card
- Update styles for improved layout and accessibility
- Adjust JavaScript to show total sponsors and visible count dynamically
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
After an import that successfully matches zero sponsors, this branch hides ImportReport because the report is only rendered in the non-empty branch below. The editor therefore drops all skipped-row reasons, contrary to the requirement to report unmatched rows. Render the report after an import even when the sponsor list is empty.
The card never visibly renders sponsor.name; it is only present in the button's accessible label and the optional logo alt text. This misses the stated listing field and leaves a completely blank card when a sponsor has no logo, level, booth, or categories. Add the sponsor name to the visible card content.
The importer explicitly allows sponsors without a featured image (with a warning), so the logo does not always identify the modal. Hiding the only rendered sponsor title leaves those modals with no visible identity and omits the sponsor name from the requested full-information view. Keep the modal title visible and account for it in the modal layout.
&-title {
// The logo already identifies the sponsor, so the title is for assistive tech only.
position: absolute;
Disabling the post-meta cache here causes the subsequent has_post_thumbnail()/get_post_meta() warning checks to load metadata separately for every matched sponsor—up to roughly 100 extra database queries per import. Batch-prime metadata once after resolving the IDs.
This client update immediately replaces the localized PHP count with hard-coded English, and every filter change remains untranslated. Use @wordpress/i18n pluralization/formatting for both count variants and rebuild the view assets so the block's text domain continues to work on localized sites.
These frontend count strings bypass the plugin text domain and immediately replace the localized PHP count on every page load. Use WordPress i18n/plural helpers for both forms and rebuild the tracked assets so translated pages do not regress to English.
When an import succeeds but every row is skipped, sponsors.length is zero, so this branch hides ImportReport and the editor never reports the unmatched rows. Show the report after an attempted import even when no sponsors matched.
The card never renders the sponsor name as visible text and explicitly suppresses categories, although the requested listing includes both. This also leaves sponsors without featured images—which the importer permits with a warning—without a visible identifier. Render the name and include categories in the card details.
The modal body has no visible sponsor name. Because missing logos only produce a warning and do not skip the sponsor, such a modal contains no visible identity at all. Add the sponsor name to the visible modal content.
limit_response_size truncates an oversized response rather than making the request fail, but the truncated body is parsed and returned as a successful import. This can silently replace the saved snapshot with only the first portion of the sheet and never report the omitted rows. Reject a body that reaches the cap (and/or compare the response length header) before parsing it.
These hard-coded English strings immediately replace the translated PHP count on every page load and after each filter change. On localized sites, this leaves the otherwise internationalized block partly in English. Build these messages with WordPress i18n/plural APIs and regenerate the compiled view assets so the required dependency is declared.
The listing omits categories because false disables them on every card, but the requested front-end listing explicitly includes each sponsor's categories. Render the shared details with categories enabled; they can still remain in the modal as well.
Using backslash as fgetcsv's escape character invokes PHP's non-RFC escaping. A valid Google Sheets quoted field ending in a backslash (for example, free-text description content) makes the closing quote look escaped and can merge subsequent columns, corrupting or skipping that sponsor row. Disable the proprietary escape mechanism for RFC 4180 CSV.
limit_response_size truncates the HTTP body rather than rejecting an oversized response. A CSV over 2 MB can therefore be parsed as a successful partial import, silently dropping later sponsors (and possibly accepting a cut-off final row). Fetch one extra byte and explicitly reject bodies above the limit before parsing.
The same logo appears beside the sponsor name on the card and beneath the sponsor-named modal heading, so this alt text makes screen readers announce the name twice in both contexts. Treat these duplicated logos as decorative with an empty alt value.
When an import matches zero sponsors, this branch hides ImportReport, so unmatched rows and their reasons are not reported—the exact case where the report is most important. Render the report in the empty state after an import as well.
The requested card listing includes each sponsor's categories, but passing false suppresses them on every card; categories are only visible after opening the modal. Use the helper's default true value here so the listing itself contains them.
update() runs immediately and replaces the server-rendered, translated _n() count with hard-coded English. This makes the result count English on every localized site, both initially and after filtering; build these messages through WordPress i18n (including plural handling) and regenerate the view bundle.
The 2 MB byte cap does not bound the number of rows. A compact sheet can produce tens of thousands of IDs here, resulting in a very large post__in SQL query and equally large block attributes, despite the stated 100-sponsor scope. Enforce the 100-row/sponsor limit before constructing this query and report excess rows.
The PR requires categories to appear on each front-end sponsor card, but passing false suppresses them because the helper only emits categories when its third argument is truthy. Remove this argument so the listing includes categories as specified; the modal output remains unchanged.
When a valid import matches zero Sponsor posts, this branch hides the ImportReport rendered only in the non-empty branch, so editors cannot see the requested skipped-row reasons. Render the report in this empty state too, guarded by importedAt to avoid showing the default 0/0 report before the first import.
update() runs immediately and replaces the server-rendered, translated _n() count with hard-coded English, so this block loses localization even before a filter is changed. Build both count formats with @wordpress/i18n (including plural handling) and regenerate the view bundle and asset dependencies.
When an import succeeds but every row is skipped, sponsors becomes empty and this branch hides ImportReport, so the editor cannot see which rows failed or why. Render the report in the empty state after an attempted import as well.
{ sponsors.length === 0 ? (
<p className="sponsor-directory-editor__empty">
{ __(
'No sponsors have been imported.',
'sponsor-directory-block'
) }
</p>
This query returns only IDs and disables meta-cache priming, but the following loop calls has_post_thumbnail() and get_post_meta() for every matched sponsor. That creates up to one additional metadata query per sponsor during a 100-row import; prime the matched IDs once before the loop.
This message is revealed dynamically after filtering, but it is not a live region, so screen-reader users receive no feedback when a filter produces zero results. Mark it as a status message so the update is announced.
<p class="sponsor-directory__no-results" data-no-results hidden><?php esc_html_e( 'No sponsors match the selected filters.', 'sponsor-directory-block' ); ?></p>
The aria-label on this generic div has no semantic role, so assistive technology may not expose “Filter sponsors” as a group label. Give the container role="group" (or use a fieldset with a legend) so the controls are announced as a related set.
When an import succeeds but every row is skipped, sponsors becomes empty and this branch hides ImportReport, so the editor cannot see the required row-level skip reasons. Render the report in the empty state after an import as well.
This no-results status is revealed dynamically after filtering, but it is not exposed as a live status message, so screen-reader users may receive no feedback that the result set became empty. Mark it as a polite status region.
<p class="sponsor-directory__no-results" data-no-results hidden><?php esc_html_e( 'No sponsors match the selected filters.', 'sponsor-directory-block' ); ?></p>
The PR requirement says categories must be visible in the sponsor listing, but passing false suppresses them on every card (they appear only in the popup). Render the shared details with its default category behavior here.
limit_response_size silently truncates oversized responses; a truncated CSV can still parse and return a successful partial import, replacing the existing snapshot without warning. Fetch one byte beyond the limit and explicitly reject any body larger than MAX_RESPONSE_SIZE before parsing.
Using backslash as fgetcsv()'s escape character misparses an otherwise valid quoted cell ending in \: it consumes the closing quote and subsequent CSV records into that field. Google Sheets emits RFC-style doubled quotes, so disable the proprietary escape character to preserve such free-text descriptions and later rows.
When every row is unmatched or invalid, sponsors.length is zero, so this branch hides ImportReport and the editor never reports why rows were skipped. Show the report in the empty state after an import so users can correct the Sheet.
The PR requires each listing card to show its categories, but passing false explicitly suppresses them and leaves categories visible only in the popup. Render the shared details with categories enabled.
limit_response_size can return a successful response whose body is silently truncated at the byte limit. Parsing and saving that prefix can report a successful import while dropping all sponsors after the cutoff; reject a body that reaches the cap before replacing the existing snapshot.
When an import succeeds but every row is skipped, sponsors.length is zero and this branch hides ImportReport, so the editor only sees “Imported 0 sponsors” and cannot see the required row-level reasons. Render the report in the empty state after an import as well.
The PR requires each sponsor in the listing to show its categories, but passing false suppresses categories on every card (they are only visible after opening the modal). Render the shared details with categories enabled.
This dynamically revealed no-results message is not exposed as a status update, so screen-reader users receive no announcement when filtering removes every sponsor. Give the message status semantics so the change is announced without moving focus.
<p class="sponsor-directory__no-results" data-no-results hidden><?php esc_html_e( 'No sponsors match the selected filters.', 'sponsor-directory-block' ); ?></p>
Each modal trigger needs an ID. The shared modal implementation reads the opener's id and later focuses that selector on close (themes/lfevents/src/js/modal.js:57,136,171); without one, keyboard focus is not returned to the sponsor card.
Wire sponsor directory into aggregate plugin workflows
This plugin's build command is not wired into the repository's documented aggregate plugin workflow: web/wp-content/themes/lfevents/package.json:13-42 has no sponsor-directory install/build commands, while docs/devguide.md:114 directs contributors to those aggregates. Add this plugin to both install-plugins and build-plugins; otherwise the standard workflow silently skips future source changes.
When an import matches zero sponsors, this branch hides ImportReport, so the required row-by-row reasons for all unmatched/skipped rows are unavailable. Render the report in the empty branch after an import (while avoiding the untouched default state), then regenerate the committed editor build asset.
When all rows are skipped, sponsors.length is zero and this branch hides ImportReport, so the editor only says no sponsors were imported and never reports which rows were unmatched or why. Render the report in the empty branch after an import, then regenerate the committed build asset.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Test page
Test Google Sheet
Test Google Sheet CSV
Initial prompt: