From d29e03971673bd3858053fddcecfce9aa83fc45e Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 7 Apr 2026 17:04:12 -0600 Subject: [PATCH] Connectors: don't clobber third-party custom render in registerDefaultConnectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 #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 #77115 --- .../plugins/connectors-js-extensibility.php | 19 +++++++++++++- .../connectors-js-extensibility/index.mjs | 24 +++++++++++++++++ routes/connectors-home/default-connectors.tsx | 17 +++++++++++- test/e2e/specs/admin/connectors.spec.js | 26 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index 8152a0d9171ff3..5e45c18fcac390 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -4,13 +4,17 @@ * Plugin URI: https://github.com/WordPress/gutenberg * Author: Gutenberg Team * - * Registers two custom-type connectors on the server: + * Registers three connectors on the server: * * 1. test_custom_service — also registered client-side via a script module using * the merging strategy (two registerConnector calls with the same slug: one * providing the render function, the other metadata). * 2. test_server_only_service — server-only, with no client-side render function, * so it should not display a card in the UI. + * 3. test_api_key_with_custom_render — an api_key connector whose JS render is + * registered *before* the page's `registerDefaultConnectors()` call runs. + * Regression test for the bug where the default ApiKeyConnector overwrote + * third-party custom renders for any api_key-authenticated connector. * * @package gutenberg-test-connectors-js-extensibility */ @@ -42,6 +46,19 @@ static function ( WP_Connector_Registry $registry ) { ), ) ); + + $registry->register( + 'test_api_key_with_custom_render', + array( + 'name' => 'Test API Key With Custom Render', + 'description' => 'An api_key connector with a JS-registered custom render.', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'api_key', + 'settingName' => 'test_api_key_with_custom_render_key', + ), + ) + ); } ); diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index 8b96ef90b474d6..68e02e12988698 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -33,3 +33,27 @@ registerConnector( 'test_custom_service', { ) ), } ); + +// Regression test: register a custom render for an api_key-authenticated +// connector. The page's `registerDefaultConnectors()` would otherwise +// overwrite this render with the generic ApiKeyConnector form because the +// store reducer spreads new config over existing entries. The fix in +// `routes/connectors-home/default-connectors.tsx` skips setting +// `args.render = ApiKeyConnector` when an existing render is present. +registerConnector( 'test_api_key_with_custom_render', { + render: ( props ) => + h( + ConnectorItem, + { + className: 'connector-item--test_api_key_with_custom_render', + name: props.name, + description: props.description, + logo: props.logo, + }, + h( + 'p', + { className: 'test-api-key-with-custom-render-content' }, + 'Custom render survived registerDefaultConnectors().' + ) + ), +} ); diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 7e212ba3846bea..0434979845b515 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -7,15 +7,18 @@ import { __experimentalRegisterConnector as registerConnector, __experimentalConnectorItem as ConnectorItem, __experimentalDefaultConnectorSettings as DefaultConnectorSettings, + privateApis as connectorsPrivateApis, type ConnectorConfig, type ConnectorRenderProps, } from '@wordpress/connectors'; +import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { Badge } from '@wordpress/ui'; /** * Internal dependencies */ +import { unlock } from '../lock-unlock'; import { useConnectorPlugin } from './use-connector-plugin'; import { OpenAILogo, @@ -25,6 +28,8 @@ import { DefaultConnectorLogo, } from './logos'; +const { store: connectorsStore } = unlock( connectorsPrivateApis ); + interface ConnectorData { name: string; description: string; @@ -256,7 +261,17 @@ export function registerDefaultConnectors() { authentication, plugin: data.plugin, }; - if ( authentication.method === 'api_key' ) { + + // If a third-party plugin's script module already registered a + // custom render for this slug (which can happen when the plugin's + // connector bundle executes before this module's dynamic import + // chain settles), don't overwrite it. The reducer spreads the + // existing entry first, so leaving `render` out of `args` here + // preserves the plugin's render while still merging the + // server-side metadata (logo, plugin install state, etc.). + const existing = + unlock( select( connectorsStore ) ).getConnector( connectorName ); + if ( authentication.method === 'api_key' && ! existing?.render ) { args.render = ApiKeyConnector; } diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index a9ce1a202c588f..562528532de89c 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -581,5 +581,31 @@ test.describe( 'Connectors', () => { card.getByText( 'A custom service for E2E testing.' ) ).toBeVisible(); } ); + + test( 'should preserve a custom render for an api_key connector registered before registerDefaultConnectors', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONNECTORS_PAGE_QUERY + ); + + const card = page.locator( + '.connector-item--test_api_key_with_custom_render' + ); + await expect( card ).toBeVisible(); + + // The custom render content must be visible — proving the + // JS-supplied render survived registerDefaultConnectors(). + await expect( + card.getByText( + 'Custom render survived registerDefaultConnectors().' + ) + ).toBeVisible(); + + // And the default API key form must NOT be present in this card. + await expect( card.getByLabel( 'API Key' ) ).toHaveCount( 0 ); + } ); } ); } );