Connectors: don't clobber third-party custom render in registerDefaultConnectors#77116
Connectors: don't clobber third-party custom render in registerDefaultConnectors#77116superdav42 wants to merge 1 commit intoWordPress:trunkfrom
Conversation
…tConnectors `registerDefaultConnectors()` runs inside the dynamically-imported `routes/connectors-home/content` module, which means it executes after any third-party plugin script module that called `__experimentalRegisterConnector()` at top level (e.g. plugins enqueueing on `options-connectors-wp-admin_init`). The connectors store reducer spreads the new config over the existing entry, so the unconditional `args.render = ApiKeyConnector` for `api_key`-authenticated providers overwrites any custom render the plugin already supplied. PR WordPress#76722 added e2e coverage for the *server-then-client* direction (where the JS register correctly wins because it runs after the default register), but didn't cover the *client-then-server* direction — which is the natural flow for plugin script modules. Fix: read the existing entry from the connectors store and only set `args.render = ApiKeyConnector` when no render has been registered yet. The reducer's spread of the existing entry preserves the plugin's render while still merging the server-side metadata (logo, plugin install state, authentication config) on top. Adds a third connector `test_api_key_with_custom_render` to the `connectors-js-extensibility` test plugin and a corresponding e2e test that asserts the custom render content is visible and the default API Key form is absent — exercising the previously-uncovered direction. Closes WordPress#77115
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @superdav42! In case you missed it, we'd love to have you join us in our Slack community. If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information. |
|
I don't have permission to apply labels — could a maintainer add |
WP core's routes/connectors-home/content.js runs registerDefaultConnectors() from inside an async dynamic import. By the time it executes, our top-level registerConnector() has already run, and the connectors store reducer spreads new config over the existing entry — so the default's `args.render = ApiKeyConnector` overwrites our custom card. The user sees the generic API-key UI instead of the endpoint URL / model picker form. The proper upstream fix is WordPress/gutenberg#77116. Until that lands and ships in a Gutenberg release, work around it by re-asserting our registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms) so we always end up last regardless of dynamic-import resolution order. Re-registration with the same render is idempotent so the redundant calls cost almost nothing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WP core's routes/connectors-home/content.js runs registerDefaultConnectors() from inside an async dynamic import, and the connectors store reducer spreads new config over existing entries — so the default's `args.render = ApiKeyConnector` can overwrite a plugin's custom render. This plugin currently dodges the bug because its JS slug (ai-provider-for-anthropic-max/connector) doesn't collide with the PHP-side provider id (anthropic-max). That's fragile — a future upstream change to slug normalization or sanitization could break it without warning. Re-assert the registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms) so we always end up last regardless of dynamic-import resolution order. Idempotent and cheap. The proper upstream fix is WordPress/gutenberg#77116. Once that ships in a Gutenberg release, this defense can be removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ix render clobber
This commit makes two coordinated changes that have to ship together:
1. RENAME the AI Client provider id from 'anthropic-max' to
'ultimate-ai-connector-anthropic-max', and the JS registerConnector()
slug from 'ai-provider-for-anthropic-max/connector' to the same
'ultimate-ai-connector-anthropic-max'. This:
- Matches the naming convention used by the sister plugins
(ultimate-ai-connector-webllm, ultimate-ai-connector-compatible-endpoints).
- Claims the 'ultimate-ai-connector-' namespace properly so a future
third-party 'anthropic-max' plugin can't collide with this one.
- Causes the WP Connectors page to render ONE card instead of two —
previously a hidden duplicate auto-registered ApiKeyConnector card
existed alongside the custom-rendered card because the slugs differed.
This is a BREAKING change for any caller that hardcoded the old
provider id (e.g. AiClient::defaultRegistry()->getProvider('anthropic-max')).
Stored OAuth tokens, REST endpoint URLs, option keys, transient prefixes,
plugin folder name, text domain, and css class are unchanged.
2. WORKAROUND for WP core's registerDefaultConnectors() clobbering custom
renders. The rename in (1) makes the slugs match, which exposes this
plugin to the same race the other Ultimate-Multisite connector plugins
hit: WP core's routes/connectors-home/content module runs
registerDefaultConnectors() inside an async dynamic import, after our
top-level registerConnector() has already populated the store, and the
reducer's spread overwrites our custom render with the generic
ApiKeyConnector. Re-assert the registration on five ticks (sync +
microtask + setTimeout 0/50/250/1000ms) so we always end up last.
The proper upstream fix is in WordPress/gutenberg#77116. Once that ships
in a Gutenberg release, the 5-tick workaround can be removed; the rename
is permanent.
CSS class on the rendered card also updated from
`connector-item--anthropic-max` to `connector-item--ultimate-ai-connector-anthropic-max`
to match the new slug and the convention used by the sister plugins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ix render clobber (#6) * v1.0.1: defensive re-register on Connectors page WP core's routes/connectors-home/content.js runs registerDefaultConnectors() from inside an async dynamic import, and the connectors store reducer spreads new config over existing entries — so the default's `args.render = ApiKeyConnector` can overwrite a plugin's custom render. This plugin currently dodges the bug because its JS slug (ai-provider-for-anthropic-max/connector) doesn't collide with the PHP-side provider id (anthropic-max). That's fragile — a future upstream change to slug normalization or sanitization could break it without warning. Re-assert the registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms) so we always end up last regardless of dynamic-import resolution order. Idempotent and cheap. The proper upstream fix is WordPress/gutenberg#77116. Once that ships in a Gutenberg release, this defense can be removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * v1.1.0: rename provider id to ultimate-ai-connector-anthropic-max + fix render clobber This commit makes two coordinated changes that have to ship together: 1. RENAME the AI Client provider id from 'anthropic-max' to 'ultimate-ai-connector-anthropic-max', and the JS registerConnector() slug from 'ai-provider-for-anthropic-max/connector' to the same 'ultimate-ai-connector-anthropic-max'. This: - Matches the naming convention used by the sister plugins (ultimate-ai-connector-webllm, ultimate-ai-connector-compatible-endpoints). - Claims the 'ultimate-ai-connector-' namespace properly so a future third-party 'anthropic-max' plugin can't collide with this one. - Causes the WP Connectors page to render ONE card instead of two — previously a hidden duplicate auto-registered ApiKeyConnector card existed alongside the custom-rendered card because the slugs differed. This is a BREAKING change for any caller that hardcoded the old provider id (e.g. AiClient::defaultRegistry()->getProvider('anthropic-max')). Stored OAuth tokens, REST endpoint URLs, option keys, transient prefixes, plugin folder name, text domain, and css class are unchanged. 2. WORKAROUND for WP core's registerDefaultConnectors() clobbering custom renders. The rename in (1) makes the slugs match, which exposes this plugin to the same race the other Ultimate-Multisite connector plugins hit: WP core's routes/connectors-home/content module runs registerDefaultConnectors() inside an async dynamic import, after our top-level registerConnector() has already populated the store, and the reducer's spread overwrites our custom render with the generic ApiKeyConnector. Re-assert the registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms) so we always end up last. The proper upstream fix is in WordPress/gutenberg#77116. Once that ships in a Gutenberg release, the 5-tick workaround can be removed; the rename is permanent. CSS class on the rendered card also updated from `connector-item--anthropic-max` to `connector-item--ultimate-ai-connector-anthropic-max` to match the new slug and the convention used by the sister plugins. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WP core's routes/connectors-home/content.js runs registerDefaultConnectors() from inside an async dynamic import. By the time it executes, our top-level registerConnector() has already run, and the connectors store reducer spreads new config over the existing entry — so the default's `args.render = ApiKeyConnector` overwrites our custom card. The user sees the generic API-key UI instead of the endpoint URL / model picker form. The proper upstream fix is WordPress/gutenberg#77116. Until that lands and ships in a Gutenberg release, work around it by re-asserting our registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms) so we always end up last regardless of dynamic-import resolution order. Re-registration with the same render is idempotent so the redundant calls cost almost nothing. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
What
registerDefaultConnectors()inroutes/connectors-home/default-connectors.tsxwas unconditionally settingargs.render = ApiKeyConnectorfor anyapi_key-authenticated connector, overwriting custom renders that third-party plugin script modules had already supplied via__experimentalRegisterConnector().This PR makes the assignment conditional on no existing render being registered, so plugin-supplied renders survive while the rest of the server-side metadata (logo, plugin install state, authentication config) still merges on top.
Why
registerDefaultConnectors()runs inside the dynamically-importedroutes/connectors-home/contentmodule. By the time it runs, every top-level plugin script module — including any that enqueued on the standardoptions-connectors-wp-admin_initaction — has already executed and registered its connectors. The store reducer spreads new config over existing entries:So whichever caller fires last wins per key, and
registerDefaultConnectors()is always last for plugins that enqueue at the standard hook. The result is that any plugin shipping a custom Connectors-page card sees it silently replaced by the genericApiKeyConnectorAPI-key form.This is particularly painful for local/self-hosted AI providers (Ollama, LM Studio, WebLLM, etc.) that don't have an API-key concept at all — the rendered form is meaningless and confusing for users.
PR #76722 added JS-extensibility e2e coverage but only for the server-then-client direction (where the test plugin's connector uses
auth: 'none', so the clobber path is never hit). The client-then-server direction forauth: 'api_key'connectors was unverified and broken — see issue #77115 for the full root-cause walkthrough and a live reproducer plugin.How
In
default-connectors.tsx, before deciding whether to assign the defaultApiKeyConnectorrender, read the current entry from the connectors store via the sameunlock( connectorsPrivateApis )patternstage.tsxalready uses:When
args.renderis omitted, the reducer's spread of the existing entry preserves whatever render the plugin already registered. Server-side metadata (name,description,logo,authentication,plugin) still merges on top of the existing entry as before.Test plan
Adds a third connector
test_api_key_with_custom_renderto theconnectors-js-extensibilitytest plugin:packages/e2e-tests/plugins/connectors-js-extensibility.php): registerstest_api_key_with_custom_renderwithtype: 'ai_provider'andauthentication.method: 'api_key'— exactly the path that hits the clobber.packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs): callsregisterConnector( 'test_api_key_with_custom_render', { render: ... } )for it. This is whatregisterDefaultConnectors()would have overwritten without the fix.test/e2e/specs/admin/connectors.spec.js): a new test inside theJS extensibilitydescribe block:'Custom render survived registerDefaultConnectors().') is visible inside the expected card.API Keylabels — i.e. the defaultApiKeyConnectorform is absent.This explicitly exercises the previously-uncovered direction. Without the source fix, the new spec fails (the card shows the API Key form instead of the custom content).
JS extensibilitytests still pass.should preserve a custom render for an api_key connector...test passes.registerConnectorworkaround.Closes #77115.