Repository navigation
Release v2.30.0 - #1246
Merged
Merged
Release v2.30.0#1246
Conversation
…-2.29.0 Merge master into development after v2.29.0
Adds the teaser entry for the new Pro "Required Words" requirement, alongside the existing Prohibited Words entry, so free users see it in the Pro feature list and the requirement is labelled correctly. Pairs with the publishpress-checklists-pro branch feature/required-words-checklist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the admin host Resolves #439. On sites where the front-end runs on a different host than wp-admin (e.g. website.com vs wp.website.com), the internal-links editor check counted zero internal links, no matter the content: JS extract_internal_links()/extract_external_links() defaulted their host to window.location.host, which in wp-admin is the admin host, never the host content actually links to. - localize the site's real hosts (home_url + site_url) as ppChecklists.internal_link_hosts and match against all of them, in both the editor JS and the PHP-side validation, via a new publishpress_checklists_internal_link_hosts filter for setups where the front-end is neither home_url() nor site_url(). - fix Internal_links::extract_internal_links(): `strpos(...) == false` is true when a link starts with the host (strpos returns 0), so such links were wrongly excluded. Changed to `!== false`. Passing an explicit $website/website still restricts matching to that one host, so existing callers of these methods are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves #1097 and #467. Counter tasks (excerpt/title/word count, tag and category count, taxonomy counts, internal/external link count) only showed pass/fail, giving no sense of how close the content was to the configured range. This adds a small live badge, e.g. "(42 / 60)", next to the task label, updating on the same tic/content-change cycle each counter already uses. Two shared helpers in meta-box.js: - format_live_count(count, min, max): "count / max" when a max is configured, "count (min N)" for a minimum-only rule, else just the count. - update_live_count(id, text): updates the badge in the metabox DOM and appends it to ppChecklists.requirements[id].label (caching the original label first, so repeated updates replace rather than stack the badge). Wired into all 9 counter-type tasks across both the classic editor and Gutenberg code paths (11 call sites): tags, categories, hierarchical and non-hierarchical taxonomies, excerpt and title character counts, word count, and internal/external link counts. image_alt_count is intentionally NOT covered: it validates each image's alt text length separately (Array.every()), so there is no single number to show. No changes needed in gutenberg-panel.jsx: the sidebar already renders requirement labels via dangerouslySetInnerHTML and re-syncs whenever 'pp-checklists.requirements-updated' fires, which every tic handler already triggers after a status check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves #453. Custom task titles were a single-line <input>, so longer instructions had to be squeezed onto one line. - Custom_item::get_setting_title_html() now renders a <textarea>, matching what Openai_item already does (same pp-checklists-custom-item-title class, which already has textarea sizing in admin-pages.css). - global-checklists.js built an <input> for newly-added custom tasks and a <textarea> only for OpenAI tasks (branched on item type). That meant a freshly-added custom task and a saved one would render differently. Both now get a <textarea>; every consumer in that file (validation, value reads) already works off the shared class selector and .val(), not the tag, so this needed no other changes. - sanitize_checklists_options() ran sanitize_text_field() over every option value, which collapses newlines, so the textarea alone would have silently discarded them on save. Added is_multiline_option() to route keys ending in "_title" (custom/OpenAI task titles - the only option keys with that suffix in this codebase) through sanitize_textarea_field() instead. - white-space: pre-line on custom task labels so saved line breaks actually render in the editor list item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues caught in review before opening the PR:
- format_live_count()'s min-only branch returned an already-
parenthesized string ("42 (min 50)"), which update_live_count()
then wrapped in another pair of parens for both the label and the
DOM badge, producing "(42 (min 50))" for every min-only counter task
(a common configuration - filled_excerpt, title_count, and most of
the count-type tasks in this plugin are typically configured with
only a minimum). Changed to a bare "42, min 50" so the single
outer wrap in update_live_count produces clean text.
- The live badge is appended into req.label, which
gutenberg-panel.jsx's normalizeLabelForSort() also uses to build the
alphabetical-sort key when "Checklist task sort order" is set to
Alphabetical. Since the badge's digits change as the user types, two
counter tasks that happen to share a label (e.g. after using the
per-task label rename feature) could visibly swap positions in the
sidebar on every keystroke. normalizeLabelForSort() now strips the
.pp-checklists-live-count element before extracting the sort text.
Rebuilt gutenberg-panel.min.js via webpack (NODE_ENV=development, to
match how the checked-in bundle is already built - production mode
produces a fully minified single line that would otherwise replace the
whole file). gutenberg-warning.min.js is untouched, matching its
unmodified .jsx source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d custom ones Caught in review before opening the PR: the white-space: pre-line rule was scoped to .pp-checklists-custom-item, which for Custom_item is NOT "this is a custom task" - filter_requirements_list() overwrites that array key with isUserRolePermitted(), i.e. whether the *viewing user's role* is allowed to complete the task. A custom task restricted to Administrators would render correctly for an Administrator but collapse back to one line for an Author viewing the identical post, since the class (and the CSS rule with it) is simply absent from the markup for that viewer. There's no reliable "is this architecturally a custom/openai task" signal to key off instead - $req['type'] is always the string 'base' (Base_requirement's default, never overridden by any subclass) - so the simplest correct fix is to apply white-space: pre-line to the .status-label of every task, not just custom ones. Built-in task labels are single-line translated strings with no embedded newlines, so this is a no-op for them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Caught in review before opening the PR (low severity, but the PHP
side already guarded against it). get_internal_link_hosts() localizes
the site's hosts and is filterable via
publishpress_checklists_internal_link_hosts for headless/multi-domain
setups. If a filter callback ever introduced an empty string, JS's
extract_internal_links()/extract_external_links() would treat it as
matching every link - link.indexOf('') is always 0 - misclassifying
every link (including genuinely external ones) as internal in the
live editor. Internal_links.php and External_links.php already guard
with $host !== '' before matching; checklists.php's
get_internal_link_hosts() and the JS get_internal_link_hosts() now
filter out empty entries too, so PHP and JS agree even under an
unusual filter callback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.5...v3.1.7) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.1 to 4.28.8. - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](browserslist/browserslist@4.28.1...4.28.8) --- updated-dependencies: - dependency-name: browserslist dependency-version: 4.28.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Clicking "No" on the "Are you sure you want to publish anyway?" modal in the Classic Editor left the whole editor unclickable until a page reload. Remodal only leaves its "opening"/"closing" state once the CSS animation fires animationstart and animationend. Both checklist modals share a single overlay element, so opening one while the other is still animating hides and re-shows that shared node mid-animation and the events are lost. The instance then stays in the "opening" state forever, and close() bails out at its first guard, so "No" does nothing: the overlay stays on top of the editor and absorbs every click, including the ones on Publish. This is also what made the modal appear to flash in twice. Rather than patching Remodal's state machine, opt these two modals out of the animation. getAnimationDuration() then reports 0 and Remodal takes its synchronous path, where the state is always settled, so the whole class of stuck-overlay bugs goes away. On top of that: - Only open a checklist modal when neither of them is open, so the two instances can no longer fight over the shared overlay. - On dismissal, reset the publishing state and re-enable the submit buttons and spinners. The handler in wp-admin/js/post.js disables them as soon as the form is submitted and never restores them when another handler blocks the submission. - Introduce the recommendation list with a "These recommendations are missing:" heading and move the question below it, so the modal says what the items under it actually are. - Drop a dead branch that printed ppChecklists.msg_missed_required. That key is never localized, so it would have rendered the literal string "undefined". The branch was unreachable, as it only runs when the list of required items is empty. Fixes #1209 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps [composer/composer](https://github.com/composer/composer) from 2.10.2 to 2.10.3. - [Release notes](https://github.com/composer/composer/releases) - [Changelog](https://github.com/composer/composer/blob/main/CHANGELOG.md) - [Commits](composer/composer@2.10.2...2.10.3) --- updated-dependencies: - dependency-name: composer/composer dependency-version: 2.10.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Serialize the complete checklist settings payload as JSON before submission so PHP can save every post type without hitting max_input_vars. Keep the decoded payload available to existing validation callbacks. Fixes #1229
WordPress 7.1 disables the Save draft button and Ctrl+S while post saving is locked. Checklists used lockPostSaving() to disable the Publish button, so drafts could not be saved while a blocking requirement was incomplete. Disable only the Publish/Update button with a body class instead. Publishing is still blocked by the savePost() filter in gutenberg-panel.jsx. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D9vmMdC7igi2xofoqVtEgE
Fix Save draft blocked by incomplete requirements on WordPress 7.1
Run it from Actions or add the build-zip label to a pull request. The zip is attached to the run as an artifact named after the ref and commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
The build steps now live in publishpress/github-workflows, so this file only declares the triggers and the plugin's inputs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CX51hBJ7p6KQ31drzHVXTx
Add a workflow that builds the plugin zip from any branch or PR
…lder The Yoast SEO module and the Util plugin-detection helpers call is_plugin_active() during init. WordPress before 6.8 only loads wp-admin/includes/plugin.php for wp-admin after init, so every admin screen failed with "Call to undefined function is_plugin_active()" when Yoast SEO was not active. Before 2.29.0, Plugin::deactivateLegacyPlugin() loaded that file on init as a side effect; removing it exposed the bug. Add Util::isPluginActive(), which loads wp-admin/includes/plugin.php when needed, and use it in all plugin-detection helpers. The Yoast module now uses Util::isYoastSeoActivated() instead of its own copy. Fixes #1238 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st-uri-3.1.7 Bump fast-uri from 3.1.5 to 3.1.7
…owserslist-4.28.8 Bump browserslist from 4.28.1 to 4.28.8
…er/composer-2.10.3 Bump composer/composer from 2.10.2 to 2.10.3
Resolve the meta-box.js conflict with #1217 by keeping both format_live_count()/update_live_count() and get_internal_link_hosts()/ normalize_link_hosts(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…slatable - Gutenberg sidebar: keep copies of the requirement objects in state and re-read ppChecklists.requirements on requirements-updated, so in-place label/status changes from meta-box.js produce a new hash and re-render. The title badge no longer stays stale while typing. - Start the word/internal/external link count caches at null so a count of 0 renders a badge on new posts. - format_live_count(): a max lower than the min is ignored, mirroring check_valid_quantity(), so min 50 / max 20 shows "42, min 50". - Badge formats are localized (live_count_max, live_count_min) and escaped before going into the label HTML. - Show the badge for the Yoast focus keyword and meta description counters. - Rebuild gutenberg-panel.min.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on one line The pre-publish panel registered its update-failed-requirements handler under the 'publishpress/checklists' namespace, which the sidebar panel removes when it unmounts, so the panel never rendered. Give it its own namespace and register it on mount. Rebuild gutenberg-warning.min.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nter Feat: show a live count next to counter checklist tasks
Brings in #1218 (live count badges). Its updateRequirements() change stores { ...req } copies, so a custom task toggled in the block editor Checklists panel now re-renders immediately instead of waiting for the next unrelated change. Conflicts: kept both the a11y strings and the live count strings in checklists.php; rebuilt gutenberg-panel.min.js and gutenberg-warning.min.js with webpack --mode development. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post editor accessibility fixes for VPAT / WCAG 2.2 AA
…#1208) Gutenberg turns off visual revisions whenever any classic meta box is registered in the block editor (and turns off real-time collaboration for meta boxes without __rtc_compatible_meta_box). The block editor no longer gets the pp_checklist_meta box; the Classic Editor keeps it unchanged. - meta-box.js still evaluates every requirement in the block editor, and add-ons (Pro, Yoast SEO, Permalinks) still extend it: its list is printed in a hidden container in the admin footer instead of a meta box. - Tasks ticked by hand (custom tasks, OpenAI tasks, Approved by, Pro's Approved by user) are saved as REST-registered post meta with the same pp_checklist_custom_item_* keys, so existing data keeps working and RTC shares the ticks between editors (no more last-save-wins). The meta is registered on every request (REST saves are not in wp-admin), and custom-fields support is added where a post type lacks it. - Ticked-by-hand tasks without REST meta (e.g. added by another plugin) are saved through admin-ajax after the post is saved. - The checklist is also shown in a "Checklist" panel in the Post sidebar, where the meta box used to be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without Pro, approved_by_user is a Pro placeholder requirement that can't be ticked. Registering its post meta made meta-box.js copy the empty meta value into the list, so a saved Required rule showed as failing and blocked publishing, while development shows it as passing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block editor saves ticked-by-hand tasks as REST post meta, whose auth only checks edit_post. An author who isn't in the "Approved by" roles (or the Pro "Approved by user" users, or a custom task's allowed roles) could approve their own post with one editPost() call or a REST request. CustomItemsMeta::userCanComplete() applies the same rules the requirements use to make a task clickable. In REST requests the write of a task the user may not tick is skipped, so the rest of the post still saves (the block editor sends every registered meta field). The Classic Editor form and the admin-ajax fallback use the same check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel keeps failedRequirements up to date from a wp.data subscription, which reads this.state.requirements. Ticking a task now edits the post meta, so the subscription runs before the new list is in the state and keeps the old result. On WordPress 6.7, unticking a required task on a published post and pressing Update right away saved the post. Validate again in the setState callback of updateRequirements(), once the new list is in the state. This also covers changes that don't touch the editor store at all (on development, unticking a required custom task on a published post never blocked Update). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a save was blocked, the panel set the edited status back to the status read when the editor opened. For a post published in the same session that is 'auto-draft', so the next Update failed with "Invalid parameter(s): status". This was already the case on development; it shows up more now that unticking a required task on a published post reliably blocks Update. Use the saved post status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1243 (GPT-6-Astra), two findings, both reproduced on SandyWP with Gutenberg 24.2 real-time collaboration: 1. Restricted approvals could be injected through RTC. Collaboration shares registered post meta with everyone who can edit the post, so an author's editPost() of pp_checklist_custom_item_approved_by showed as ticked in an editor's session, and the editor's save of an unrelated change stored it. The write guard only knew the saver. Restricted tasks ("Approved by", Pro's "Approved by user", custom tasks limited to some roles) are no longer REST post meta, so they are never in the shared state or in a post save. The user who clicks one saves it at once through pp_checklists_save_custom_items, which checks the nonce, edit_post, the post type and that this user may tick the task. Other editors get the saved state through the Heartbeat API (about 5-10 s). Tasks anyone may tick stay REST meta and keep syncing live. 2. With the custom-fields opt-out, ticks were lost. The meta was registered but not exposed, so the browser used the admin-ajax fallback, and the handler skipped the task because it was registered and still answered success. Tasks are now registered as REST meta only where the post type exposes it, the browser and the server use the same rule (CustomItemsMeta::isRestItem()), and every other task is saved per click. The admin-ajax save sends only the task the user clicked (the old fallback re-sent every task, which could overwrite another user's approval), accepts only yes/no, saves all or nothing, and returns an error instead of success when it refuses. On an error the task goes back to its previous state and an error notice is shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no-metabox Block editor: no Checklist meta box, so visual revisions and RTC work (#1208)
The site imports the line break as <br>, splitting the sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne-line Join CHANGELOG.md introduction onto one line
- Add .phpcs.xml (based on publishpress-future) and .phpcs-plugin-review.xml (needed by the second, WordPress.org plugin-review pass of lint:phpcs). - Exclude the three naming sniffs until a major version: Pro and third-party code use those class and method names. - Exclude the 31 phpcbf-fixable formatting codes for now, with a TODO for a phpcbf pass after 2.30. - Add explicit public visibility to class constants. - Review every nonce-verification finding: annotate read-only uses, and in save_global_checklist() verify the nonce before reading the form. - Use wp_parse_url(), wp_json_encode(), get_stylesheet_directory() and get_template_directory(); annotate the remaining intentional findings. - Run the code-standards workflow on pull requests and put needs: [code-check] back on the WordPress.org deploy job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI creates .cache/wp_test inside the repository, and both lint:phpcs passes scanned its mu-plugin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add PHPCS rulesets and restore the code-standards deploy gate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…task" buttons WordPress core sets ".button .dashicons" to line-height 1.9 (38px for a 20px icon), so the icon was drawn below the label. Lay the buttons out with inline-flex and give the icon a normal line-height. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he plugin is bundled inside Pro #1238
…tattributes
Replace inconsistent `defined('ABSPATH') or die()` guards with standardized `\defined('ABSPATH') || exit;` syntax across all core files. Add .phpcs.xml, README.md, dist, and node_modules to .gitattributes export-ignore list.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Release v2.30.0
What this PR changes
publishpress-checklists.php: plugin headerVersionandPPCH_VERSIONset to 2.30.0readme.txt:Stable tag: 2.30.0andTested up to: 7.1CHANGELOG.md: 2.30.0 entry (date set to 20 October, 2026; check it on release day)Not listed in the changelog (internal only): Dependabot bumps #1224 #1225 #1227 and the build-zip workflow #1237. Issues #1169, #1212 and #1215 were closed in the milestone without merged code, so they are not listed either.
Checks run locally on this branch
composer check:php(PHP 5.6 to 8.5): passedcomposer lint:php: passed, 140 filescomposer lint:phpcs(both passes, .phpcs.xml and .phpcs-plugin-review.xml): passed, no findingscomposer build: builtpublishpress-checklists-2.30.0.zip, and the packaged plugin carries 2.30.0npx webpack: the rebuilt JS bundles match the committed ones byte for byteStill to do before release (per #1199)
Companion Pro PR: publishpress/publishpress-checklists-pro#180
🤖 Generated with Claude Code