From 2112fa819786a93d2fa8cf1371597c77393c066f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 17:06:43 +0000 Subject: [PATCH 01/81] Initial route registration with extensibility script loading example --- .../connections/connections-extension.js | 9 ++++++++ lib/experimental/connections/load.php | 22 +++++++++++++++++++ lib/init.php | 20 +++++++++++++++++ lib/load.php | 1 + package-lock.json | 13 +++++++++++ package.json | 3 ++- routes/connections-home/package.json | 16 ++++++++++++++ routes/connections-home/route.ts | 8 +++++++ routes/connections-home/stage.tsx | 22 +++++++++++++++++++ 9 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 lib/experimental/connections/connections-extension.js create mode 100644 lib/experimental/connections/load.php create mode 100644 routes/connections-home/package.json create mode 100644 routes/connections-home/route.ts create mode 100644 routes/connections-home/stage.tsx diff --git a/lib/experimental/connections/connections-extension.js b/lib/experimental/connections/connections-extension.js new file mode 100644 index 00000000000000..e4f825b82915c6 --- /dev/null +++ b/lib/experimental/connections/connections-extension.js @@ -0,0 +1,9 @@ +/** + * Example extension script for the Connections page. + * Demonstrates how plugins can hook into the page without a build step. + */ +( function () { + 'use strict'; + // eslint-disable-next-line no-console + console.log( 'Hello from a possible plugin extension!' ); +} )(); diff --git a/lib/experimental/connections/load.php b/lib/experimental/connections/load.php new file mode 100644 index 00000000000000..71d302028ea3d5 --- /dev/null +++ b/lib/experimental/connections/load.php @@ -0,0 +1,22 @@ + __( 'Connections' ), +}; diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx new file mode 100644 index 00000000000000..0c1e5ab1c2969e --- /dev/null +++ b/routes/connections-home/stage.tsx @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { Page } from '@wordpress/admin-ui'; +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +function ConnectionsPage() { + return ( + +
+ +
+
+ ); +} + +function Stage() { + return ; +} + +export const stage = Stage; From 6604d0d3a1296f55e6eb58e15349b221f447f335 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 17:22:48 +0000 Subject: [PATCH 02/81] bootstrap UI --- routes/connections-home/stage.tsx | 67 ++++++++++++++++++++++++++++-- routes/connections-home/style.scss | 61 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 routes/connections-home/style.scss diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 0c1e5ab1c2969e..67bccfce166540 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -5,11 +5,72 @@ import { Page } from '@wordpress/admin-ui'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import './style.scss'; + +// OpenAI logo as inline SVG +const OpenAILogo = () => ( + + + +); + +function ConnectorItem( { + icon, + name, + description, +}: { + icon: React.ReactNode; + name: string; + description: string; +} ) { + return ( +
+
{ icon }
+
+
{ name }
+
+ { description } +
+
+
+ +
+
+ ); +} + function ConnectionsPage() { return ( - -
- + +
+
+
+ } + name={ __( 'OpenAI' ) } + description={ __( + 'Text, image, and code generation with GPT and DALL-E.' + ) } + /> +
+
); diff --git a/routes/connections-home/style.scss b/routes/connections-home/style.scss new file mode 100644 index 00000000000000..350215e2cea282 --- /dev/null +++ b/routes/connections-home/style.scss @@ -0,0 +1,61 @@ +.connections-page { + max-width: 800px; + margin: 0 auto; + padding: 24px; +} + +.connections-card { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 8px 24px; +} + +.connections-connectors-list { + display: flex; + flex-direction: column; +} + +.connections-connector-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 0; + border-top: 1px solid #e0e0e0; + + &:first-child { + border-top: none; + } +} + +.connections-connector-item__icon { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #1e1e1e; +} + +.connections-connector-item__content { + flex: 1; + min-width: 0; +} + +.connections-connector-item__name { + font-size: 14px; + font-weight: 600; + color: #1e1e1e; + margin-bottom: 2px; +} + +.connections-connector-item__description { + font-size: 13px; + color: #757575; + line-height: 1.4; +} + +.connections-connector-item__action { + flex-shrink: 0; +} From eea90e28eb32d3b4a9c6ec2614b86df5ad100f86 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 17:27:20 +0000 Subject: [PATCH 03/81] base components version --- routes/connections-home/stage.tsx | 52 +++++++++++++++------------ routes/connections-home/style.scss | 56 ------------------------------ 2 files changed, 29 insertions(+), 79 deletions(-) diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 67bccfce166540..243f693c47a577 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -2,7 +2,15 @@ * WordPress dependencies */ import { Page } from '@wordpress/admin-ui'; -import { Button } from '@wordpress/components'; +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalItemGroup as ItemGroup, + __experimentalItem as Item, + __experimentalText as Text, + FlexBlock, + Button, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** @@ -36,18 +44,18 @@ function ConnectorItem( { description: string; } ) { return ( -
-
{ icon }
-
-
{ name }
-
- { description } -
-
-
+ + + { icon } + + + { name } + { description } + + -
-
+ + ); } @@ -60,17 +68,15 @@ function ConnectionsPage() { ) } >
-
-
- } - name={ __( 'OpenAI' ) } - description={ __( - 'Text, image, and code generation with GPT and DALL-E.' - ) } - /> -
-
+ + } + name={ __( 'OpenAI' ) } + description={ __( + 'Text, image, and code generation with GPT and DALL-E.' + ) } + /> +
); diff --git a/routes/connections-home/style.scss b/routes/connections-home/style.scss index 350215e2cea282..b71af56098ba24 100644 --- a/routes/connections-home/style.scss +++ b/routes/connections-home/style.scss @@ -3,59 +3,3 @@ margin: 0 auto; padding: 24px; } - -.connections-card { - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 8px 24px; -} - -.connections-connectors-list { - display: flex; - flex-direction: column; -} - -.connections-connector-item { - display: flex; - align-items: center; - gap: 16px; - padding: 16px 0; - border-top: 1px solid #e0e0e0; - - &:first-child { - border-top: none; - } -} - -.connections-connector-item__icon { - flex-shrink: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - color: #1e1e1e; -} - -.connections-connector-item__content { - flex: 1; - min-width: 0; -} - -.connections-connector-item__name { - font-size: 14px; - font-weight: 600; - color: #1e1e1e; - margin-bottom: 2px; -} - -.connections-connector-item__description { - font-size: 13px; - color: #757575; - line-height: 1.4; -} - -.connections-connector-item__action { - flex-shrink: 0; -} From 2045880b705fc16ec160b0ee8571b8434bc3c163 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 17:41:30 +0000 Subject: [PATCH 04/81] Improve UI --- routes/connections-home/stage.tsx | 6 ++++-- routes/connections-home/style.scss | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 243f693c47a577..0e013546694f1a 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -45,7 +45,7 @@ function ConnectorItem( { } ) { return ( - + { icon } @@ -53,7 +53,9 @@ function ConnectorItem( { { description } - + ); diff --git a/routes/connections-home/style.scss b/routes/connections-home/style.scss index b71af56098ba24..10fd4b55204058 100644 --- a/routes/connections-home/style.scss +++ b/routes/connections-home/style.scss @@ -2,4 +2,13 @@ max-width: 800px; margin: 0 auto; padding: 24px; + + .components-item-group { + border-radius: 8px; + } + + .components-item { + padding: 16px 20px; + border-radius: 8px; + } } From 2621e633dbdc6f9527f6780c77d1a1194041feb4 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 17:50:07 +0000 Subject: [PATCH 05/81] rename to connectors --- lib/experimental/connections/load.php | 22 ------------------- .../connectors-extension.js} | 2 +- lib/experimental/connectors/load.php | 22 +++++++++++++++++++ lib/init.php | 12 +++++----- lib/load.php | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) delete mode 100644 lib/experimental/connections/load.php rename lib/experimental/{connections/connections-extension.js => connectors/connectors-extension.js} (80%) create mode 100644 lib/experimental/connectors/load.php diff --git a/lib/experimental/connections/load.php b/lib/experimental/connections/load.php deleted file mode 100644 index 71d302028ea3d5..00000000000000 --- a/lib/experimental/connections/load.php +++ /dev/null @@ -1,22 +0,0 @@ - Date: Mon, 23 Feb 2026 18:11:40 +0000 Subject: [PATCH 06/81] expandable draft state --- routes/connections-home/package.json | 4 +- routes/connections-home/stage.tsx | 67 +++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/routes/connections-home/package.json b/routes/connections-home/package.json index 0fdb2701fbc80c..dd27d75e685ff4 100644 --- a/routes/connections-home/package.json +++ b/routes/connections-home/package.json @@ -11,6 +11,8 @@ "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", "@wordpress/components": "file:../../packages/components", - "@wordpress/i18n": "file:../../packages/i18n" + "@wordpress/element": "file:../../packages/element", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/icons": "file:../../packages/icons" } } diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 0e013546694f1a..69c857ad011c70 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -10,7 +10,10 @@ import { __experimentalText as Text, FlexBlock, Button, + TextControl, } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { chevronUp, chevronDown } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** @@ -43,20 +46,62 @@ function ConnectorItem( { name: string; description: string; } ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + const [ apiKey, setApiKey ] = useState( '' ); + return ( - - { icon } - - - { name } - { description } + + + { icon } + + + { name } + { description } + + + + + + { isExpanded && ( + + + + + + - - - + ) } + ); } From fb4ba8d0e3eee631e6b571d4f37bf392de056571 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 19:46:30 +0000 Subject: [PATCH 07/81] tmp connector api via global From 8214ea77e8fbf9f8643bfe9d6ede2403b5101ab6 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 19:46:41 +0000 Subject: [PATCH 08/81] tmp connector global api --- .../connectors/connectors-extension.js | 40 ++++- routes/connections-home/api.ts | 48 ++++++ routes/connections-home/connector-item.tsx | 101 ++++++++++++ routes/connections-home/package.json | 1 + routes/connections-home/stage.tsx | 155 ++++++++---------- routes/connections-home/store.ts | 70 ++++++++ routes/connections-home/types.ts | 22 +++ 7 files changed, 351 insertions(+), 86 deletions(-) create mode 100644 routes/connections-home/api.ts create mode 100644 routes/connections-home/connector-item.tsx create mode 100644 routes/connections-home/store.ts create mode 100644 routes/connections-home/types.ts diff --git a/lib/experimental/connectors/connectors-extension.js b/lib/experimental/connectors/connectors-extension.js index e69a773d6746a8..c36c79af92eb54 100644 --- a/lib/experimental/connectors/connectors-extension.js +++ b/lib/experimental/connectors/connectors-extension.js @@ -1,9 +1,45 @@ /** * Example extension script for the Connectors page. * Demonstrates how plugins can hook into the page without a build step. + * + * This script registers a "Hello World" connector using the wp.connectors API. */ ( function () { 'use strict'; - // eslint-disable-next-line no-console - console.log( 'Hello from a possible plugin extension!' ); + + /** + * Register the Hello World connector. + * Retries if the API isn't available yet. + */ + function registerHelloWorldConnector() { + // Check if wp.connectors API is available + if ( + typeof wp === 'undefined' || + typeof wp.connectors === 'undefined' || + typeof wp.connectors.registerConnector !== 'function' + ) { + setTimeout( registerHelloWorldConnector, 100 ); + return; + } + + // Register the Hello World connector using the public API + wp.connectors.registerConnector( 'example/hello-world', { + label: 'Hello World', + description: + 'A simple example connector registered via vanilla JS.', + } ); + + // eslint-disable-next-line no-console + console.log( 'Hello World connector registered!' ); + } + + // Start trying to register + if ( document.readyState === 'loading' ) { + document.addEventListener( + 'DOMContentLoaded', + registerHelloWorldConnector + ); + } else { + registerHelloWorldConnector(); + } } )(); diff --git a/routes/connections-home/api.ts b/routes/connections-home/api.ts new file mode 100644 index 00000000000000..602dee4c4f2278 --- /dev/null +++ b/routes/connections-home/api.ts @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store, STORE_NAME } from './store'; +import { ConnectorItem, DefaultConnectorSettings } from './connector-item'; +import type { ConnectorConfig, ConnectorRenderProps } from './types'; + +export type { ConnectorConfig, ConnectorRenderProps }; + +/** + * Register a connector thatdsfsdfdsfdsfdsfds will appear in the Connectors settings page. + * + * @param slug Unique identifier for the connector. + * @param config Connector configuration. + * + * @example + * ```js + * import { registerConnector, ConnectorItem } from '@wordpress/connectors'; + * + * registerConnector( 'my-plugin/openai', { + * label: 'OpenAI', + * description: 'Text, image, and code generation with GPT.', + * icon: , + * render: ( { slug, label, description } ) => ( + * } + * name={ label } + * description={ description } + * > + * + * + * ), + * } ); + * ``` + */ +export function registerConnector( + slug: string, + config: Omit< ConnectorConfig, 'slug' > +): void { + dispatch( store ).registerConnector( slug, config ); +} + +export { ConnectorItem, DefaultConnectorSettings, store, STORE_NAME }; diff --git a/routes/connections-home/connector-item.tsx b/routes/connections-home/connector-item.tsx new file mode 100644 index 00000000000000..3763166d1398b9 --- /dev/null +++ b/routes/connections-home/connector-item.tsx @@ -0,0 +1,101 @@ +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalItem as Item, + __experimentalText as Text, + FlexBlock, + Button, + TextControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { chevronUp, chevronDown } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { ReactNode } from 'react'; + +export interface ConnectorItemProps { + icon?: ReactNode; + name: string; + description: string; + children?: ReactNode; +} + +export function ConnectorItem( { + icon, + name, + description, + children, +}: ConnectorItemProps ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return ( + + + + { icon } + + + { name } + { description } + + + + + + { isExpanded && children } + + + ); +} + +/** + * Default settings form for connectors that don't provide custom render. + */ +export function DefaultConnectorSettings( { + onSave, + onCancel, +}: { + onSave?: ( apiKey: string ) => void; + onCancel?: () => void; +} ) { + const [ apiKey, setApiKey ] = useState( '' ); + + return ( + + + + + + + + ); +} diff --git a/routes/connections-home/package.json b/routes/connections-home/package.json index dd27d75e685ff4..1dd5f608c8689e 100644 --- a/routes/connections-home/package.json +++ b/routes/connections-home/package.json @@ -11,6 +11,7 @@ "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", "@wordpress/components": "file:../../packages/components", + "@wordpress/data": "file:../../packages/data", "@wordpress/element": "file:../../packages/element", "@wordpress/i18n": "file:../../packages/i18n", "@wordpress/icons": "file:../../packages/icons" diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 69c857ad011c70..e1bc42e286b950 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -3,23 +3,37 @@ */ import { Page } from '@wordpress/admin-ui'; import { - __experimentalHStack as HStack, - __experimentalVStack as VStack, __experimentalItemGroup as ItemGroup, - __experimentalItem as Item, - __experimentalText as Text, - FlexBlock, - Button, - TextControl, } from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { chevronUp, chevronDown } from '@wordpress/icons'; +import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import './style.scss'; +import { + registerConnector, + ConnectorItem, + DefaultConnectorSettings, + store, +} from './api'; + +// Expose the public API globally for vanilla JS plugins +declare global { + interface Window { + wp: { + connectors?: { + registerConnector: typeof registerConnector; + }; + }; + } +} + +window.wp = window.wp || {}; +window.wp.connectors = { + registerConnector, +}; // OpenAI logo as inline SVG const OpenAILogo = () => ( @@ -37,76 +51,33 @@ const OpenAILogo = () => ( ); -function ConnectorItem( { - icon, - name, - description, -}: { - icon: React.ReactNode; - name: string; - description: string; -} ) { - const [ isExpanded, setIsExpanded ] = useState( false ); - const [ apiKey, setApiKey ] = useState( '' ); - - return ( - - - - { icon } - - - { name } - { description } - - - - +// Register built-in OpenAI connector +registerConnector( 'core/openai', { + label: __( 'OpenAI' ), + description: __( 'Text, image, and code generation with GPT and DALL-E.' ), + icon: , + render: ( { label, description } ) => ( + } + name={ label } + description={ description } + > + { + // eslint-disable-next-line no-console + console.log( 'Saving OpenAI API key:', apiKey ); + } } + /> + + ), +} ); - { isExpanded && ( - - - - - - - - ) } - - +function ConnectorsPage() { + const connectors = useSelect( + ( select ) => select( store ).getConnectors(), + [] ); -} -function ConnectionsPage() { return (
- } - name={ __( 'OpenAI' ) } - description={ __( - 'Text, image, and code generation with GPT and DALL-E.' - ) } - /> + { connectors.map( ( connector ) => { + if ( connector.render ) { + return ( + + ); + } + // Default rendering for connectors without custom render + return ( + + + + ); + } ) }
@@ -130,7 +117,7 @@ function ConnectionsPage() { } function Stage() { - return ; + return ; } export const stage = Stage; diff --git a/routes/connections-home/store.ts b/routes/connections-home/store.ts new file mode 100644 index 00000000000000..57ec3a815c417e --- /dev/null +++ b/routes/connections-home/store.ts @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import type { ConnectorConfig, ConnectorsState } from './types'; + +const STORE_NAME = 'core/connectors'; + +const DEFAULT_STATE: ConnectorsState = { + connectors: {}, +}; + +const actions = { + registerConnector( slug: string, config: Omit< ConnectorConfig, 'slug' > ) { + return { + type: 'REGISTER_CONNECTOR' as const, + slug, + config, + }; + }, +}; + +type Action = ReturnType< typeof actions.registerConnector >; + +function reducer( + state: ConnectorsState = DEFAULT_STATE, + action: Action +): ConnectorsState { + switch ( action.type ) { + case 'REGISTER_CONNECTOR': + return { + ...state, + connectors: { + ...state.connectors, + [ action.slug ]: { + slug: action.slug, + ...action.config, + }, + }, + }; + default: + return state; + } +} + +const selectors = { + getConnectors( state: ConnectorsState ): ConnectorConfig[] { + return Object.values( state.connectors ); + }, + getConnector( + state: ConnectorsState, + slug: string + ): ConnectorConfig | undefined { + return state.connectors[ slug ]; + }, +}; + +export const store = createReduxStore( STORE_NAME, { + reducer, + actions, + selectors, +} ); + +register( store ); + +export { STORE_NAME }; diff --git a/routes/connections-home/types.ts b/routes/connections-home/types.ts new file mode 100644 index 00000000000000..1b0b72068cd22a --- /dev/null +++ b/routes/connections-home/types.ts @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + +export interface ConnectorRenderProps { + slug: string; + label: string; + description: string; +} + +export interface ConnectorConfig { + slug: string; + label: string; + description: string; + icon?: ReactNode; + render?: ( props: ConnectorRenderProps ) => ReactNode; +} + +export interface ConnectorsState { + connectors: Record< string, ConnectorConfig >; +} From 1ead5a0e99c42e7781d16bc1994edeec7296a5b3 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 19:58:10 +0000 Subject: [PATCH 09/81] script modules approaach --- .../connectors/connectors-extension.js | 50 ++++--------------- lib/experimental/connectors/load.php | 26 ++++++++++ lib/init.php | 19 ------- package-lock.json | 17 +++++++ packages/connectors/package.json | 23 +++++++++ .../connectors/src}/api.ts | 2 +- .../connectors/src}/connector-item.tsx | 0 packages/connectors/src/index.ts | 4 ++ .../connectors/src}/store.ts | 0 .../connectors/src}/types.ts | 0 routes/connections-home/package.json | 5 +- routes/connections-home/stage.tsx | 34 ++++--------- 12 files changed, 94 insertions(+), 86 deletions(-) create mode 100644 packages/connectors/package.json rename {routes/connections-home => packages/connectors/src}/api.ts (93%) rename {routes/connections-home => packages/connectors/src}/connector-item.tsx (100%) create mode 100644 packages/connectors/src/index.ts rename {routes/connections-home => packages/connectors/src}/store.ts (100%) rename {routes/connections-home => packages/connectors/src}/types.ts (100%) diff --git a/lib/experimental/connectors/connectors-extension.js b/lib/experimental/connectors/connectors-extension.js index c36c79af92eb54..86279daf070272 100644 --- a/lib/experimental/connectors/connectors-extension.js +++ b/lib/experimental/connectors/connectors-extension.js @@ -1,45 +1,17 @@ /** * Example extension script for the Connectors page. - * Demonstrates how plugins can hook into the page without a build step. + * Demonstrates how plugins can register a connector using the @wordpress/connectors API. * - * This script registers a "Hello World" connector using the wp.connectors API. + * This script registers a "Hello World" connector to show how plugins can + * add their own connectors to the Connectors settings page. */ -( function () { - 'use strict'; +import { registerConnector } from '@wordpress/connectors'; - /** - * Register the Hello World connector. - * Retries if the API isn't available yet. - */ - function registerHelloWorldConnector() { - // Check if wp.connectors API is available - if ( - typeof wp === 'undefined' || - typeof wp.connectors === 'undefined' || - typeof wp.connectors.registerConnector !== 'function' - ) { - setTimeout( registerHelloWorldConnector, 100 ); - return; - } +// Register the Hello World connector +registerConnector( 'example/hello-world', { + label: 'Hello World', + description: 'A simple example connector registered via script module.', +} ); - // Register the Hello World connector using the public API - wp.connectors.registerConnector( 'example/hello-world', { - label: 'Hello World', - description: - 'A simple example connector registered via vanilla JS.', - } ); - - // eslint-disable-next-line no-console - console.log( 'Hello World connector registered!' ); - } - - // Start trying to register - if ( document.readyState === 'loading' ) { - document.addEventListener( - 'DOMContentLoaded', - registerHelloWorldConnector - ); - } else { - registerHelloWorldConnector(); - } -} )(); +// eslint-disable-next-line no-console +console.log( 'Hello World connector registered!' ); diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index b2b134436e95f5..447cd0e217e2d8 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -20,3 +20,29 @@ function gutenberg_connectors_add_settings_menu_item() { 'gutenberg_connections_wp_admin_render_page' ); } + +/** + * Registers the example connectors extension as a script module. + */ +function gutenberg_register_connectors_extension_module() { + wp_register_script_module( + 'gutenberg/connectors-extension', + gutenberg_url( 'lib/experimental/connectors/connectors-extension.js' ), + array( '@wordpress/connectors' ), + filemtime( __DIR__ . '/connectors-extension.js' ) + ); +} +add_action( 'init', 'gutenberg_register_connectors_extension_module' ); + +/** + * Enqueues the connectors extension on the Connectors page. + * + * @param string $hook_suffix The current admin page. + */ +function gutenberg_enqueue_connectors_extension( $hook_suffix ) { + if ( 'settings_page_connections-wp-admin' !== $hook_suffix ) { + return; + } + wp_enqueue_script_module( 'gutenberg/connectors-extension' ); +} +add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_connectors_extension' ); diff --git a/lib/init.php b/lib/init.php index e2dc90788bb4ed..8d9f92cd127955 100644 --- a/lib/init.php +++ b/lib/init.php @@ -58,22 +58,3 @@ function gutenberg_menu() { } add_action( 'admin_menu', 'gutenberg_menu', 9 ); -/** - * Enqueues the Connectors page extension script. - * - * @param string $hook_suffix The current admin page. - */ -function gutenberg_enqueue_connectors_extension( $hook_suffix ) { - if ( 'settings_page_connections-wp-admin' !== $hook_suffix ) { - return; - } - - wp_enqueue_script( - 'gutenberg-connectors-extension', - plugins_url( 'lib/experimental/connectors/connectors-extension.js', dirname( __FILE__ ) ), - array(), - filemtime( plugin_dir_path( dirname( __FILE__ ) ) . 'lib/experimental/connectors/connectors-extension.js' ), - true - ); -} -add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_connectors_extension' ); diff --git a/package-lock.json b/package-lock.json index 2c61e7fa6fc0e8..ec6e774c37e5d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20771,6 +20771,10 @@ "resolved": "routes/connections-home", "link": true }, + "node_modules/@wordpress/connectors": { + "resolved": "packages/connectors", + "link": true + }, "node_modules/@wordpress/core-abilities": { "resolved": "packages/core-abilities", "link": true @@ -58774,6 +58778,17 @@ "tiny-emitter": "^2.0.0" } }, + "packages/connectors": { + "name": "@wordpress/connectors", + "version": "1.0.0", + "dependencies": { + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons" + } + }, "packages/core-abilities": { "name": "@wordpress/core-abilities", "version": "0.5.0", @@ -62978,6 +62993,8 @@ "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", "@wordpress/components": "file:../../packages/components", + "@wordpress/connectors": "file:../../packages/connectors", + "@wordpress/data": "file:../../packages/data", "@wordpress/i18n": "file:../../packages/i18n" } }, diff --git a/packages/connectors/package.json b/packages/connectors/package.json new file mode 100644 index 00000000000000..b0c066184ccfca --- /dev/null +++ b/packages/connectors/package.json @@ -0,0 +1,23 @@ +{ + "name": "@wordpress/connectors", + "version": "1.0.0", + "private": true, + "description": "Connectors API for WordPress plugins.", + "module": "build-module/index.mjs", + "exports": { + ".": { + "types": "./build-types/index.d.ts", + "import": "./build-module/index.mjs" + }, + "./package.json": "./package.json" + }, + "wpScriptModuleExports": "./build-module/index.mjs", + "types": "build-types", + "dependencies": { + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons" + } +} diff --git a/routes/connections-home/api.ts b/packages/connectors/src/api.ts similarity index 93% rename from routes/connections-home/api.ts rename to packages/connectors/src/api.ts index 602dee4c4f2278..dac1928882c132 100644 --- a/routes/connections-home/api.ts +++ b/packages/connectors/src/api.ts @@ -13,7 +13,7 @@ import type { ConnectorConfig, ConnectorRenderProps } from './types'; export type { ConnectorConfig, ConnectorRenderProps }; /** - * Register a connector thatdsfsdfdsfdsfdsfds will appear in the Connectors settings page. + * Register a connector that will appear in the Connectors settings page. * * @param slug Unique identifier for the connector. * @param config Connector configuration. diff --git a/routes/connections-home/connector-item.tsx b/packages/connectors/src/connector-item.tsx similarity index 100% rename from routes/connections-home/connector-item.tsx rename to packages/connectors/src/connector-item.tsx diff --git a/packages/connectors/src/index.ts b/packages/connectors/src/index.ts new file mode 100644 index 00000000000000..fef8b30dfaa1fe --- /dev/null +++ b/packages/connectors/src/index.ts @@ -0,0 +1,4 @@ +export { registerConnector } from './api'; +export { ConnectorItem, DefaultConnectorSettings } from './connector-item'; +export { store, STORE_NAME } from './store'; +export type { ConnectorConfig, ConnectorRenderProps } from './types'; diff --git a/routes/connections-home/store.ts b/packages/connectors/src/store.ts similarity index 100% rename from routes/connections-home/store.ts rename to packages/connectors/src/store.ts diff --git a/routes/connections-home/types.ts b/packages/connectors/src/types.ts similarity index 100% rename from routes/connections-home/types.ts rename to packages/connectors/src/types.ts diff --git a/routes/connections-home/package.json b/routes/connections-home/package.json index 1dd5f608c8689e..e1d5e93b3b2a29 100644 --- a/routes/connections-home/package.json +++ b/routes/connections-home/package.json @@ -11,9 +11,8 @@ "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", "@wordpress/components": "file:../../packages/components", + "@wordpress/connectors": "file:../../packages/connectors", "@wordpress/data": "file:../../packages/data", - "@wordpress/element": "file:../../packages/element", - "@wordpress/i18n": "file:../../packages/i18n", - "@wordpress/icons": "file:../../packages/icons" + "@wordpress/i18n": "file:../../packages/i18n" } } diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index e1bc42e286b950..990b607d75f006 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -5,6 +5,14 @@ import { Page } from '@wordpress/admin-ui'; import { __experimentalItemGroup as ItemGroup, } from '@wordpress/components'; +import { + registerConnector, + ConnectorItem, + DefaultConnectorSettings, + store, + type ConnectorRenderProps, + type ConnectorConfig, +} from '@wordpress/connectors'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -12,28 +20,6 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import './style.scss'; -import { - registerConnector, - ConnectorItem, - DefaultConnectorSettings, - store, -} from './api'; - -// Expose the public API globally for vanilla JS plugins -declare global { - interface Window { - wp: { - connectors?: { - registerConnector: typeof registerConnector; - }; - }; - } -} - -window.wp = window.wp || {}; -window.wp.connectors = { - registerConnector, -}; // OpenAI logo as inline SVG const OpenAILogo = () => ( @@ -56,7 +42,7 @@ registerConnector( 'core/openai', { label: __( 'OpenAI' ), description: __( 'Text, image, and code generation with GPT and DALL-E.' ), icon: , - render: ( { label, description } ) => ( + render: ( { label, description }: ConnectorRenderProps ) => ( } name={ label } @@ -87,7 +73,7 @@ function ConnectorsPage() { >
- { connectors.map( ( connector ) => { + { connectors.map( ( connector: ConnectorConfig ) => { if ( connector.render ) { return ( Date: Mon, 23 Feb 2026 20:13:02 +0000 Subject: [PATCH 10/81] script module pattern working --- .../connectors/connectors-extension.js | 36 ++++++++++++- package-lock.json | 4 +- packages/connectors/src/connector-item.tsx | 21 ++------ routes/connections-home/package.json | 4 +- routes/connections-home/stage.tsx | 51 ++++++++++++++----- 5 files changed, 84 insertions(+), 32 deletions(-) diff --git a/lib/experimental/connectors/connectors-extension.js b/lib/experimental/connectors/connectors-extension.js index 86279daf070272..49b0d71e687bea 100644 --- a/lib/experimental/connectors/connectors-extension.js +++ b/lib/experimental/connectors/connectors-extension.js @@ -4,13 +4,47 @@ * * This script registers a "Hello World" connector to show how plugins can * add their own connectors to the Connectors settings page. + * + * Note: @wordpress/connectors is imported as a script module, while + * wp.element and wp.components are accessed as globals (no build step needed). */ -import { registerConnector } from '@wordpress/connectors'; + +/* global wp */ +import { registerConnector, ConnectorItem } from '@wordpress/connectors'; + +const { useState, createElement } = wp.element; +const { Button } = wp.components; + +// Hello World connector render component +function HelloWorldConnector( { label, description } ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return createElement( + ConnectorItem, + { + name: label, + description, + actionArea: createElement( + Button, + { + variant: 'secondary', + size: 'compact', + onClick: () => setIsExpanded( ! isExpanded ), + 'aria-expanded': isExpanded, + }, + isExpanded ? 'Close' : 'Configure' + ), + }, + isExpanded && + createElement( 'p', null, 'Hello World settings would go here!' ) + ); +} // Register the Hello World connector registerConnector( 'example/hello-world', { label: 'Hello World', description: 'A simple example connector registered via script module.', + render: HelloWorldConnector, } ); // eslint-disable-next-line no-console diff --git a/package-lock.json b/package-lock.json index ec6e774c37e5d2..c4f8994ca314dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62995,7 +62995,9 @@ "@wordpress/components": "file:../../packages/components", "@wordpress/connectors": "file:../../packages/connectors", "@wordpress/data": "file:../../packages/data", - "@wordpress/i18n": "file:../../packages/i18n" + "@wordpress/element": "file:../../packages/element", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/icons": "file:../../packages/icons" } }, "routes/font-list": { diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 3763166d1398b9..609a307e619fd5 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -11,7 +11,6 @@ import { TextControl, } from '@wordpress/components'; import { useState } from '@wordpress/element'; -import { chevronUp, chevronDown } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** @@ -23,6 +22,7 @@ export interface ConnectorItemProps { icon?: ReactNode; name: string; description: string; + actionArea?: ReactNode; children?: ReactNode; } @@ -30,10 +30,9 @@ export function ConnectorItem( { icon, name, description, + actionArea, children, }: ConnectorItemProps ) { - const [ isExpanded, setIsExpanded ] = useState( false ); - return ( @@ -45,26 +44,16 @@ export function ConnectorItem( { { description } - + { actionArea } - - { isExpanded && children } + { children } ); } /** - * Default settings form for connectors that don't provide custom render. + * Default settings form for connectors. */ export function DefaultConnectorSettings( { onSave, diff --git a/routes/connections-home/package.json b/routes/connections-home/package.json index e1d5e93b3b2a29..4280f86abf09cb 100644 --- a/routes/connections-home/package.json +++ b/routes/connections-home/package.json @@ -13,6 +13,8 @@ "@wordpress/components": "file:../../packages/components", "@wordpress/connectors": "file:../../packages/connectors", "@wordpress/data": "file:../../packages/data", - "@wordpress/i18n": "file:../../packages/i18n" + "@wordpress/element": "file:../../packages/element", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/icons": "file:../../packages/icons" } } diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 990b607d75f006..08ae8e0e11f73f 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -4,6 +4,7 @@ import { Page } from '@wordpress/admin-ui'; import { __experimentalItemGroup as ItemGroup, + Button, } from '@wordpress/components'; import { registerConnector, @@ -14,6 +15,8 @@ import { type ConnectorConfig, } from '@wordpress/connectors'; import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { chevronUp, chevronDown } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** @@ -37,25 +40,47 @@ const OpenAILogo = () => ( ); -// Register built-in OpenAI connector -registerConnector( 'core/openai', { - label: __( 'OpenAI' ), - description: __( 'Text, image, and code generation with GPT and DALL-E.' ), - icon: , - render: ( { label, description }: ConnectorRenderProps ) => ( +// OpenAI connector render component +function OpenAIConnector( { label, description }: ConnectorRenderProps ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return ( } name={ label } description={ description } + actionArea={ + + } > - { - // eslint-disable-next-line no-console - console.log( 'Saving OpenAI API key:', apiKey ); - } } - /> + { isExpanded && ( + { + // eslint-disable-next-line no-console + console.log( 'Saving OpenAI API key:', apiKey ); + } } + onCancel={ () => setIsExpanded( false ) } + /> + ) } - ), + ); +} + +// Register built-in OpenAI connector +registerConnector( 'core/openai', { + label: __( 'OpenAI' ), + description: __( 'Text, image, and code generation with GPT and DALL-E.' ), + icon: , + render: OpenAIConnector, } ); function ConnectorsPage() { From e4dc487185487147c4a57fe72982348274b480d1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 20:43:31 +0000 Subject: [PATCH 11/81] working connection api with an example --- routes/connections-home/stage.tsx | 21 ++++----------------- routes/connections-home/style.scss | 7 ++++++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 08ae8e0e11f73f..ef27562bffd4b6 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -2,10 +2,7 @@ * WordPress dependencies */ import { Page } from '@wordpress/admin-ui'; -import { - __experimentalItemGroup as ItemGroup, - Button, -} from '@wordpress/components'; +import { __experimentalVStack as VStack, Button } from '@wordpress/components'; import { registerConnector, ConnectorItem, @@ -97,7 +94,7 @@ function ConnectorsPage() { ) } >
- + { connectors.map( ( connector: ConnectorConfig ) => { if ( connector.render ) { return ( @@ -109,19 +106,9 @@ function ConnectorsPage() { /> ); } - // Default rendering for connectors without custom render - return ( - - - - ); + return null; } ) } - +
); diff --git a/routes/connections-home/style.scss b/routes/connections-home/style.scss index 10fd4b55204058..3acff752ff029f 100644 --- a/routes/connections-home/style.scss +++ b/routes/connections-home/style.scss @@ -4,11 +4,16 @@ padding: 24px; .components-item-group { - border-radius: 8px; + gap: 12px; + display: flex; + flex-direction: column; } .components-item { padding: 16px 20px; border-radius: 8px; + border: 1px solid #ddd; + background: #fff; + overflow: hidden; } } From 2392c0f04d36674d414c92952c08aab9874e3ebe Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 20:48:03 +0000 Subject: [PATCH 12/81] remove unrequired prop --- routes/connections-home/stage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index ef27562bffd4b6..09dd684cc44cf3 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -76,7 +76,6 @@ function OpenAIConnector( { label, description }: ConnectorRenderProps ) { registerConnector( 'core/openai', { label: __( 'OpenAI' ), description: __( 'Text, image, and code generation with GPT and DALL-E.' ), - icon: , render: OpenAIConnector, } ); From 9e8398bbc89228feec50558bc3de344ca35142fb Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 21:33:47 +0000 Subject: [PATCH 13/81] extract defaults to separate file --- .../connections-home/default-connectors.tsx | 206 ++++++++++++++++++ routes/connections-home/stage.tsx | 73 +------ 2 files changed, 211 insertions(+), 68 deletions(-) create mode 100644 routes/connections-home/default-connectors.tsx diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx new file mode 100644 index 00000000000000..2c7d1a3a469995 --- /dev/null +++ b/routes/connections-home/default-connectors.tsx @@ -0,0 +1,206 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; +import { + registerConnector, + ConnectorItem, + DefaultConnectorSettings, + type ConnectorRenderProps, +} from '@wordpress/connectors'; +import { useState } from '@wordpress/element'; +import { chevronUp, chevronDown } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +// OpenAI logo as inline SVG +const OpenAILogo = () => ( + + + +); + +// Claude/Anthropic logo as inline SVG +const ClaudeLogo = () => ( + + + +); + +// Gemini logo as inline SVG +const GeminiLogo = () => ( + + + + + + + + + +); + +// OpenAI connector render component +function OpenAIConnector( { label, description }: ConnectorRenderProps ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return ( + } + name={ label } + description={ description } + actionArea={ + + } + > + { isExpanded && ( + { + // eslint-disable-next-line no-console + console.log( 'Saving OpenAI API key:', apiKey ); + } } + onCancel={ () => setIsExpanded( false ) } + /> + ) } + + ); +} + +// Claude connector render component +function ClaudeConnector( { label, description }: ConnectorRenderProps ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return ( + } + name={ label } + description={ description } + actionArea={ + + } + > + { isExpanded && ( + { + // eslint-disable-next-line no-console + console.log( 'Saving Claude API key:', apiKey ); + } } + onCancel={ () => setIsExpanded( false ) } + /> + ) } + + ); +} + +// Gemini connector render component +function GeminiConnector( { label, description }: ConnectorRenderProps ) { + const [ isExpanded, setIsExpanded ] = useState( false ); + + return ( + } + name={ label } + description={ description } + actionArea={ + + } + > + { isExpanded && ( + { + // eslint-disable-next-line no-console + console.log( 'Saving Gemini API key:', apiKey ); + } } + onCancel={ () => setIsExpanded( false ) } + /> + ) } + + ); +} + +// Register built-in connectors +export function registerDefaultConnectors() { + registerConnector( 'core/openai', { + label: __( 'OpenAI' ), + description: __( + 'Text, image, and code generation with GPT and DALL-E.' + ), + render: OpenAIConnector, + } ); + + registerConnector( 'core/claude', { + label: __( 'Claude' ), + description: __( 'Writing, research, and analysis with Claude.' ), + render: ClaudeConnector, + } ); + + registerConnector( 'core/gemini', { + label: __( 'Gemini' ), + description: __( + "Content generation, translation, and vision with Google's Gemini." + ), + render: GeminiConnector, + } ); +} diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 09dd684cc44cf3..927c221ac4697f 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -2,82 +2,19 @@ * WordPress dependencies */ import { Page } from '@wordpress/admin-ui'; -import { __experimentalVStack as VStack, Button } from '@wordpress/components'; -import { - registerConnector, - ConnectorItem, - DefaultConnectorSettings, - store, - type ConnectorRenderProps, - type ConnectorConfig, -} from '@wordpress/connectors'; +import { __experimentalVStack as VStack } from '@wordpress/components'; +import { store, type ConnectorConfig } from '@wordpress/connectors'; import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { chevronUp, chevronDown } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import './style.scss'; +import { registerDefaultConnectors } from './default-connectors'; -// OpenAI logo as inline SVG -const OpenAILogo = () => ( - - - -); - -// OpenAI connector render component -function OpenAIConnector( { label, description }: ConnectorRenderProps ) { - const [ isExpanded, setIsExpanded ] = useState( false ); - - return ( - } - name={ label } - description={ description } - actionArea={ - - } - > - { isExpanded && ( - { - // eslint-disable-next-line no-console - console.log( 'Saving OpenAI API key:', apiKey ); - } } - onCancel={ () => setIsExpanded( false ) } - /> - ) } - - ); -} - -// Register built-in OpenAI connector -registerConnector( 'core/openai', { - label: __( 'OpenAI' ), - description: __( 'Text, image, and code generation with GPT and DALL-E.' ), - render: OpenAIConnector, -} ); +// Register built-in connectors +registerDefaultConnectors(); function ConnectorsPage() { const connectors = useSelect( From 6a543af89a72ec42f6c47c6347be6d1b1c869873 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 21:38:40 +0000 Subject: [PATCH 14/81] fix gemini logo --- .../connections-home/default-connectors.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index 2c7d1a3a469995..233d5cd5557434 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -49,29 +49,26 @@ const GeminiLogo = () => ( - - - - - - - + + + + ); From 1812aa7c5bf4a2e9d4d6d503c38a4184a79157f5 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 23 Feb 2026 21:39:23 +0000 Subject: [PATCH 15/81] fix open ai logo --- routes/connections-home/default-connectors.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index 233d5cd5557434..979a6336f6a810 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -15,8 +15,8 @@ import { __ } from '@wordpress/i18n'; // OpenAI logo as inline SVG const OpenAILogo = () => ( Date: Mon, 23 Feb 2026 21:59:00 +0000 Subject: [PATCH 16/81] properly activate and install gemini --- .../connections-home/default-connectors.tsx | 117 +++++++++++++++++- routes/connections-home/package.json | 1 + 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index 979a6336f6a810..6eeee4f261c2da 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import apiFetch from '@wordpress/api-fetch'; import { Button } from '@wordpress/components'; import { registerConnector, @@ -8,7 +9,7 @@ import { DefaultConnectorSettings, type ConnectorRenderProps, } from '@wordpress/connectors'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { chevronUp, chevronDown } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; @@ -142,9 +143,109 @@ function ClaudeConnector( { label, description }: ConnectorRenderProps ) { ); } +type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active'; + // Gemini connector render component function GeminiConnector( { label, description }: ConnectorRenderProps ) { + const [ pluginStatus, setPluginStatus ] = + useState< PluginStatus >( 'checking' ); const [ isExpanded, setIsExpanded ] = useState( false ); + const [ isBusy, setIsBusy ] = useState( false ); + + // Check plugin status on mount + useEffect( () => { + const checkPluginStatus = async () => { + try { + const plugins = await apiFetch< + Array< { plugin: string; status: string } > + >( { + path: '/wp/v2/plugins', + } ); + + const googleAiPlugin = plugins.find( + ( p ) => p.plugin === 'google-ai-provider/plugin' + ); + + if ( ! googleAiPlugin ) { + setPluginStatus( 'not-installed' ); + } else if ( googleAiPlugin.status === 'active' ) { + setPluginStatus( 'active' ); + } else { + setPluginStatus( 'inactive' ); + } + } catch { + // If we can't check, assume not installed + setPluginStatus( 'not-installed' ); + } + }; + + checkPluginStatus(); + }, [] ); + + const installPlugin = async () => { + setIsBusy( true ); + try { + await apiFetch( { + method: 'POST', + path: '/wp/v2/plugins', + data: { slug: 'google-ai-provider', status: 'active' }, + } ); + setPluginStatus( 'active' ); + setIsExpanded( true ); + } catch { + // Handle error (could show notice) + } finally { + setIsBusy( false ); + } + }; + + const activatePlugin = async () => { + setIsBusy( true ); + try { + await apiFetch( { + method: 'PUT', + path: '/wp/v2/plugins/google-ai-provider/plugin', + data: { status: 'active' }, + } ); + setPluginStatus( 'active' ); + setIsExpanded( true ); + } catch { + // Handle error + } finally { + setIsBusy( false ); + } + }; + + const handleButtonClick = () => { + if ( pluginStatus === 'not-installed' ) { + installPlugin(); + } else if ( pluginStatus === 'inactive' ) { + activatePlugin(); + } else { + setIsExpanded( ! isExpanded ); + } + }; + + const getButtonLabel = () => { + if ( isBusy ) { + return pluginStatus === 'not-installed' + ? __( 'Installing…' ) + : __( 'Activating…' ); + } + if ( isExpanded ) { + return __( 'Close' ); + } + switch ( pluginStatus ) { + case 'checking': + return __( 'Checking…' ); + case 'not-installed': + return __( 'Install' ); + case 'inactive': + return __( 'Activate' ); + case 'active': + return __( 'Set up' ); + } + }; return ( setIsExpanded( ! isExpanded ) } + onClick={ handleButtonClick } + disabled={ pluginStatus === 'checking' || isBusy } + isBusy={ isBusy } aria-expanded={ isExpanded } > - { isExpanded ? __( 'Close' ) : __( 'Install' ) } + { getButtonLabel() } } > - { isExpanded && ( + { isExpanded && pluginStatus === 'active' && ( { // eslint-disable-next-line no-console diff --git a/routes/connections-home/package.json b/routes/connections-home/package.json index 4280f86abf09cb..64a8c0c1cea73a 100644 --- a/routes/connections-home/package.json +++ b/routes/connections-home/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", + "@wordpress/api-fetch": "file:../../packages/api-fetch", "@wordpress/components": "file:../../packages/components", "@wordpress/connectors": "file:../../packages/connectors", "@wordpress/data": "file:../../packages/data", From 160e2490a8b0fbea19bc58c835eaa50c26cf8699 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 24 Feb 2026 10:14:00 +0100 Subject: [PATCH 17/81] Update the package lock file --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index c4f8994ca314dd..19ae1a1256d4f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62992,6 +62992,7 @@ "version": "1.0.0", "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", + "@wordpress/api-fetch": "file:../../packages/api-fetch", "@wordpress/components": "file:../../packages/components", "@wordpress/connectors": "file:../../packages/connectors", "@wordpress/data": "file:../../packages/data", From 9f6d184fe42314f6d82fba4a420cc7bc5ef270e8 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 24 Feb 2026 10:19:17 +0100 Subject: [PATCH 18/81] Fix the reported PHPCS issue --- lib/init.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/init.php b/lib/init.php index 8d9f92cd127955..13ca26d4b9e83c 100644 --- a/lib/init.php +++ b/lib/init.php @@ -57,4 +57,3 @@ function gutenberg_menu() { ); } add_action( 'admin_menu', 'gutenberg_menu', 9 ); - From df488894de69bff3292ee46255356b34f39bf357 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 24 Feb 2026 10:36:00 +0100 Subject: [PATCH 19/81] Position Connectors menu item after General in Settings Add position parameter (1) to add_submenu_page so the Connectors menu item appears between General and Writing in the Settings submenu, per Matt's direction. Co-Authored-By: Claude Opus 4.6 --- lib/experimental/connectors/load.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index 447cd0e217e2d8..aa79f6ea22e4e9 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -17,7 +17,8 @@ function gutenberg_connectors_add_settings_menu_item() { __( 'Connectors', 'gutenberg' ), 'manage_options', 'connections-wp-admin', - 'gutenberg_connections_wp_admin_render_page' + 'gutenberg_connections_wp_admin_render_page', + 1 ); } From 22c72e2b1d0d7a195977d39f0c056c53dc35a6ce Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 24 Feb 2026 10:37:29 +0100 Subject: [PATCH 20/81] Add plugin directory search link to Connectors page Add a helpful message at the bottom of the connectors list pointing users to the plugin directory to find additional connector plugins. Co-Authored-By: Claude Opus 4.6 --- routes/connections-home/stage.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/routes/connections-home/stage.tsx b/routes/connections-home/stage.tsx index 927c221ac4697f..218eff8c5fe558 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connections-home/stage.tsx @@ -5,6 +5,7 @@ import { Page } from '@wordpress/admin-ui'; import { __experimentalVStack as VStack } from '@wordpress/components'; import { store, type ConnectorConfig } from '@wordpress/connectors'; import { useSelect } from '@wordpress/data'; +import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -45,6 +46,19 @@ function ConnectorsPage() { return null; } ) } +

+ { createInterpolateElement( + __( + 'If the provider you need is not listed, search the plugin directory to see if a connector is available.' + ), + { + a: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + } + ) } +

); From 84bd4ee235354e301a86f00d7d683568336da5e1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 10:24:09 +0000 Subject: [PATCH 21/81] fix gemini slug --- routes/connections-home/default-connectors.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index 6eeee4f261c2da..bb8a875aa71a6b 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -163,7 +163,7 @@ function GeminiConnector( { label, description }: ConnectorRenderProps ) { } ); const googleAiPlugin = plugins.find( - ( p ) => p.plugin === 'google-ai-provider/plugin' + ( p ) => p.plugin === 'ai-provider-for-google/plugin' ); if ( ! googleAiPlugin ) { @@ -188,7 +188,7 @@ function GeminiConnector( { label, description }: ConnectorRenderProps ) { await apiFetch( { method: 'POST', path: '/wp/v2/plugins', - data: { slug: 'google-ai-provider', status: 'active' }, + data: { slug: 'ai-provider-for-google', status: 'active' }, } ); setPluginStatus( 'active' ); setIsExpanded( true ); @@ -204,7 +204,7 @@ function GeminiConnector( { label, description }: ConnectorRenderProps ) { try { await apiFetch( { method: 'PUT', - path: '/wp/v2/plugins/google-ai-provider/plugin', + path: '/wp/v2/plugins/ai-provider-for-google/plugin', data: { status: 'active' }, } ); setPluginStatus( 'active' ); From b630db46edade7369132a0de27f6dac65e1ad080 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 10:49:04 +0000 Subject: [PATCH 22/81] initial gemini connection --- .../connectors/gemini-connector.php | 74 +++++++++++++++++++ lib/experimental/connectors/load.php | 2 + packages/connectors/src/connector-item.tsx | 5 +- .../connections-home/default-connectors.tsx | 36 ++++++++- 4 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 lib/experimental/connectors/gemini-connector.php diff --git a/lib/experimental/connectors/gemini-connector.php b/lib/experimental/connectors/gemini-connector.php new file mode 100644 index 00000000000000..f0f127066ece76 --- /dev/null +++ b/lib/experimental/connectors/gemini-connector.php @@ -0,0 +1,74 @@ + 'string', + 'default' => '', + 'show_in_rest' => true, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); +} + +/** + * Passes a connector API key to the WP AI client. + * + * Base function that can be used by any provider. + * + * @param string $option_name The option name for the API key. + * @param string $provider_id The WP AI client provider ID. + */ +function gutenberg_pass_connector_key_to_ai_client( $option_name, $provider_id ) { + // Only run if WP AI client is available (WordPress 6.8+). + if ( ! class_exists( 'Jeeo\AiClient\AiClient' ) ) { + return; + } + + $api_key = get_option( $option_name, '' ); + + if ( empty( $api_key ) ) { + return; + } + + $registry = \Jeeo\AiClient\AiClient::defaultRegistry(); + + if ( ! $registry->hasProvider( $provider_id ) ) { + return; + } + + $registry->setProviderRequestAuthentication( + $provider_id, + new \Jeeo\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $api_key ) + ); +} + +/** + * Registers the Gemini connector setting. + */ +function gutenberg_register_gemini_connector_setting() { + gutenberg_register_connector_api_key_setting( 'connectors_gemini_api_key' ); +} +add_action( 'init', 'gutenberg_register_gemini_connector_setting' ); + +/** + * Passes the Gemini API key to the WP AI client. + */ +function gutenberg_pass_gemini_key_to_ai_client() { + gutenberg_pass_connector_key_to_ai_client( 'connectors_gemini_api_key', 'google' ); +} +add_action( 'init', 'gutenberg_pass_gemini_key_to_ai_client', 20 ); diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index aa79f6ea22e4e9..8be5a59e99d2f6 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -47,3 +47,5 @@ function gutenberg_enqueue_connectors_extension( $hook_suffix ) { wp_enqueue_script_module( 'gutenberg/connectors-extension' ); } add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_connectors_extension' ); + +require __DIR__ . '/gemini-connector.php'; diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 609a307e619fd5..56e931790a6c47 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -58,11 +58,13 @@ export function ConnectorItem( { export function DefaultConnectorSettings( { onSave, onCancel, + initialValue = '', }: { onSave?: ( apiKey: string ) => void; onCancel?: () => void; + initialValue?: string; } ) { - const [ apiKey, setApiKey ] = useState( '' ); + const [ apiKey, setApiKey ] = useState( initialValue ); return ( @@ -72,7 +74,6 @@ export function DefaultConnectorSettings( { value={ apiKey } onChange={ setApiKey } placeholder={ __( 'Enter your API key' ) } - type="password" /> } > - { isExpanded && ( + { isExpanded && pluginStatus === 'active' && ( { - // eslint-disable-next-line no-console - console.log( 'Saving OpenAI API key:', apiKey ); + initialValue={ currentApiKey } + onSave={ async ( apiKey: string ) => { + await saveApiKey( apiKey ); + setIsExpanded( false ); } } onCancel={ () => setIsExpanded( false ) } /> @@ -110,7 +132,19 @@ function OpenAIConnector( { label, description }: ConnectorRenderProps ) { // Claude connector render component function ClaudeConnector( { label, description }: ConnectorRenderProps ) { - const [ isExpanded, setIsExpanded ] = useState( false ); + const { + pluginStatus, + isExpanded, + setIsExpanded, + isBusy, + currentApiKey, + handleButtonClick, + getButtonLabel, + saveApiKey, + } = useConnectorPlugin( { + pluginSlug: 'ai-provider-for-anthropic', + settingName: 'connectors_anthropic_api_key', + } ); return ( setIsExpanded( ! isExpanded ) } + onClick={ handleButtonClick } + disabled={ pluginStatus === 'checking' || isBusy } + isBusy={ isBusy } aria-expanded={ isExpanded } > - { isExpanded ? __( 'Close' ) : __( 'Install' ) } + { getButtonLabel() } } > - { isExpanded && ( + { isExpanded && pluginStatus === 'active' && ( { - // eslint-disable-next-line no-console - console.log( 'Saving Claude API key:', apiKey ); + initialValue={ currentApiKey } + onSave={ async ( apiKey: string ) => { + await saveApiKey( apiKey ); + setIsExpanded( false ); } } onCancel={ () => setIsExpanded( false ) } /> @@ -143,126 +184,21 @@ function ClaudeConnector( { label, description }: ConnectorRenderProps ) { ); } -type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active'; - // Gemini connector render component function GeminiConnector( { label, description }: ConnectorRenderProps ) { - const [ pluginStatus, setPluginStatus ] = - useState< PluginStatus >( 'checking' ); - const [ isExpanded, setIsExpanded ] = useState( false ); - const [ isBusy, setIsBusy ] = useState( false ); - const [ currentApiKey, setCurrentApiKey ] = useState( '' ); - - // Fetch the current API key - const fetchApiKey = async () => { - try { - const settings = await apiFetch< { - connectors_gemini_api_key?: string; - } >( { - path: '/wp/v2/settings', - } ); - setCurrentApiKey( settings.connectors_gemini_api_key || '' ); - } catch { - // Ignore errors - } - }; - - // Check plugin status on mount - useEffect( () => { - const checkPluginStatus = async () => { - try { - const plugins = await apiFetch< - Array< { plugin: string; status: string } > - >( { - path: '/wp/v2/plugins', - } ); - - const googleAiPlugin = plugins.find( - ( p ) => p.plugin === 'ai-provider-for-google/plugin' - ); - - if ( ! googleAiPlugin ) { - setPluginStatus( 'not-installed' ); - } else if ( googleAiPlugin.status === 'active' ) { - setPluginStatus( 'active' ); - // Fetch API key when plugin is active - fetchApiKey(); - } else { - setPluginStatus( 'inactive' ); - } - } catch { - // If we can't check, assume not installed - setPluginStatus( 'not-installed' ); - } - }; - - checkPluginStatus(); - }, [] ); - - const installPlugin = async () => { - setIsBusy( true ); - try { - await apiFetch( { - method: 'POST', - path: '/wp/v2/plugins', - data: { slug: 'ai-provider-for-google', status: 'active' }, - } ); - setPluginStatus( 'active' ); - setIsExpanded( true ); - } catch { - // Handle error (could show notice) - } finally { - setIsBusy( false ); - } - }; - - const activatePlugin = async () => { - setIsBusy( true ); - try { - await apiFetch( { - method: 'PUT', - path: '/wp/v2/plugins/ai-provider-for-google/plugin', - data: { status: 'active' }, - } ); - setPluginStatus( 'active' ); - setIsExpanded( true ); - } catch { - // Handle error - } finally { - setIsBusy( false ); - } - }; - - const handleButtonClick = () => { - if ( pluginStatus === 'not-installed' ) { - installPlugin(); - } else if ( pluginStatus === 'inactive' ) { - activatePlugin(); - } else { - setIsExpanded( ! isExpanded ); - } - }; - - const getButtonLabel = () => { - if ( isBusy ) { - return pluginStatus === 'not-installed' - ? __( 'Installing…' ) - : __( 'Activating…' ); - } - if ( isExpanded ) { - return __( 'Close' ); - } - switch ( pluginStatus ) { - case 'checking': - return __( 'Checking…' ); - case 'not-installed': - return __( 'Install' ); - case 'inactive': - return __( 'Activate' ); - case 'active': - return __( 'Set up' ); - } - }; + const { + pluginStatus, + isExpanded, + setIsExpanded, + isBusy, + currentApiKey, + handleButtonClick, + getButtonLabel, + saveApiKey, + } = useConnectorPlugin( { + pluginSlug: 'ai-provider-for-google', + settingName: 'connectors_gemini_api_key', + } ); return ( { - try { - await apiFetch( { - method: 'POST', - path: '/wp/v2/settings', - data: { - connectors_gemini_api_key: apiKey, - }, - } ); - setCurrentApiKey( apiKey ); - setIsExpanded( false ); - } catch ( error ) { - // eslint-disable-next-line no-console - console.error( 'Failed to save API key:', error ); - } + await saveApiKey( apiKey ); + setIsExpanded( false ); } } onCancel={ () => setIsExpanded( false ) } /> diff --git a/routes/connections-home/use-connector-plugin.ts b/routes/connections-home/use-connector-plugin.ts new file mode 100644 index 00000000000000..bda65fdbcb4714 --- /dev/null +++ b/routes/connections-home/use-connector-plugin.ts @@ -0,0 +1,170 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active'; + +interface UseConnectorPluginOptions { + pluginSlug: string; + settingName: string; +} + +interface UseConnectorPluginReturn { + pluginStatus: PluginStatus; + isExpanded: boolean; + setIsExpanded: ( expanded: boolean ) => void; + isBusy: boolean; + currentApiKey: string; + handleButtonClick: () => void; + getButtonLabel: () => string; + saveApiKey: ( apiKey: string ) => Promise< void >; +} + +export function useConnectorPlugin( { + pluginSlug, + settingName, +}: UseConnectorPluginOptions ): UseConnectorPluginReturn { + const [ pluginStatus, setPluginStatus ] = + useState< PluginStatus >( 'checking' ); + const [ isExpanded, setIsExpanded ] = useState( false ); + const [ isBusy, setIsBusy ] = useState( false ); + const [ currentApiKey, setCurrentApiKey ] = useState( '' ); + + // Fetch the current API key + const fetchApiKey = useCallback( async () => { + try { + const settings = await apiFetch< Record< string, string > >( { + path: '/wp/v2/settings', + } ); + setCurrentApiKey( settings[ settingName ] || '' ); + } catch { + // Ignore errors + } + }, [ settingName ] ); + + // Check plugin status on mount + useEffect( () => { + const checkPluginStatus = async () => { + try { + const plugins = await apiFetch< + Array< { plugin: string; status: string } > + >( { + path: '/wp/v2/plugins', + } ); + + const plugin = plugins.find( + ( p ) => p.plugin === `${ pluginSlug }/plugin` + ); + + if ( ! plugin ) { + setPluginStatus( 'not-installed' ); + } else if ( plugin.status === 'active' ) { + setPluginStatus( 'active' ); + fetchApiKey(); + } else { + setPluginStatus( 'inactive' ); + } + } catch { + setPluginStatus( 'not-installed' ); + } + }; + + checkPluginStatus(); + }, [ pluginSlug, fetchApiKey ] ); + + const installPlugin = async () => { + setIsBusy( true ); + try { + await apiFetch( { + method: 'POST', + path: '/wp/v2/plugins', + data: { slug: pluginSlug, status: 'active' }, + } ); + setPluginStatus( 'active' ); + setIsExpanded( true ); + } catch { + // Handle error + } finally { + setIsBusy( false ); + } + }; + + const activatePlugin = async () => { + setIsBusy( true ); + try { + await apiFetch( { + method: 'PUT', + path: `/wp/v2/plugins/${ pluginSlug }/plugin`, + data: { status: 'active' }, + } ); + setPluginStatus( 'active' ); + setIsExpanded( true ); + } catch { + // Handle error + } finally { + setIsBusy( false ); + } + }; + + const handleButtonClick = () => { + if ( pluginStatus === 'not-installed' ) { + installPlugin(); + } else if ( pluginStatus === 'inactive' ) { + activatePlugin(); + } else { + setIsExpanded( ! isExpanded ); + } + }; + + const getButtonLabel = () => { + if ( isBusy ) { + return pluginStatus === 'not-installed' + ? __( 'Installing…' ) + : __( 'Activating…' ); + } + if ( isExpanded ) { + return __( 'Close' ); + } + switch ( pluginStatus ) { + case 'checking': + return __( 'Checking…' ); + case 'not-installed': + return __( 'Install' ); + case 'inactive': + return __( 'Activate' ); + case 'active': + return __( 'Set up' ); + } + }; + + const saveApiKey = async ( apiKey: string ) => { + try { + await apiFetch( { + method: 'POST', + path: '/wp/v2/settings', + data: { + [ settingName ]: apiKey, + }, + } ); + setCurrentApiKey( apiKey ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Failed to save API key:', error ); + throw error; + } + }; + + return { + pluginStatus, + isExpanded, + setIsExpanded, + isBusy, + currentApiKey, + handleButtonClick, + getButtonLabel, + saveApiKey, + }; +} From 6a5806b3377ccc0020b4dc2becf1f8126b858a5a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 13:09:05 +0000 Subject: [PATCH 27/81] UI polishing --- packages/connectors/src/connector-item.tsx | 34 +++++++++++----- .../connections-home/default-connectors.tsx | 40 ++++++------------- .../connections-home/use-connector-plugin.ts | 2 +- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 56e931790a6c47..b6ab59fbe7674a 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -6,6 +6,7 @@ import { __experimentalVStack as VStack, __experimentalItem as Item, __experimentalText as Text, + ExternalLink, FlexBlock, Button, TextControl, @@ -52,18 +53,22 @@ export function ConnectorItem( { ); } +export interface DefaultConnectorSettingsProps { + onSave?: ( apiKey: string ) => void; + initialValue?: string; + helpUrl?: string; + helpLabel?: string; +} + /** * Default settings form for connectors. */ export function DefaultConnectorSettings( { onSave, - onCancel, initialValue = '', -}: { - onSave?: ( apiKey: string ) => void; - onCancel?: () => void; - initialValue?: string; -} ) { + helpUrl, + helpLabel, +}: DefaultConnectorSettingsProps ) { const [ apiKey, setApiKey ] = useState( initialValue ); return ( @@ -73,12 +78,19 @@ export function DefaultConnectorSettings( { label={ __( 'API Key' ) } value={ apiKey } onChange={ setApiKey } - placeholder={ __( 'Enter your API key' ) } + placeholder="YOUR_API_KEY" + help={ + helpUrl ? ( + <> + { __( 'Get your API key at' ) }{ ' ' } + + { helpLabel || helpUrl.replace( /^https?:\/\//, '' ) } + + + ) : undefined + } /> - - + - + ) : ( + + + + ) } ); } diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index ec992f64cb1a73..a28b0bf19ffea5 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; +import { + __experimentalHStack as HStack, + Button, +} from '@wordpress/components'; import { registerConnector, ConnectorItem, @@ -15,6 +18,22 @@ import { __ } from '@wordpress/i18n'; */ import { useConnectorPlugin } from './use-connector-plugin'; +const ConnectedBadge = () => ( + + { __( 'Connected' ) } + +); + // OpenAI logo as inline SVG const OpenAILogo = () => ( ( ); -// OpenAI connector render component -function OpenAIConnector( { label, description }: ConnectorRenderProps ) { +interface ConnectorConfig { + pluginSlug: string; + settingName: string; + helpUrl: string; + helpLabel: string; + Logo: React.ComponentType; +} + +function ProviderConnector( { + label, + description, + pluginSlug, + settingName, + helpUrl, + helpLabel, + Logo, +}: ConnectorRenderProps & ConnectorConfig ) { const { pluginStatus, isExpanded, setIsExpanded, isBusy, + isConnected, currentApiKey, handleButtonClick, getButtonLabel, saveApiKey, + removeApiKey, } = useConnectorPlugin( { - pluginSlug: 'ai-provider-for-openai', - settingName: 'connectors_openai_api_key', + pluginSlug, + settingName, } ); return ( } + icon={ } name={ label } description={ description } actionArea={ - + + { isConnected && ! isExpanded && } + + } > { isExpanded && pluginStatus === 'active' && ( { await saveApiKey( apiKey ); setIsExpanded( false ); @@ -162,101 +213,45 @@ function OpenAIConnector( { label, description }: ConnectorRenderProps ) { ); } -// Claude connector render component -function ClaudeConnector( { label, description }: ConnectorRenderProps ) { - const { - pluginStatus, - isExpanded, - setIsExpanded, - isBusy, - currentApiKey, - handleButtonClick, - getButtonLabel, - saveApiKey, - } = useConnectorPlugin( { - pluginSlug: 'ai-provider-for-anthropic', - settingName: 'connectors_anthropic_api_key', - } ); +// OpenAI connector render component +function OpenAIConnector( props: ConnectorRenderProps ) { + return ( + + ); +} +// Claude connector render component +function ClaudeConnector( props: ConnectorRenderProps ) { return ( - } - name={ label } - description={ description } - actionArea={ - - } - > - { isExpanded && pluginStatus === 'active' && ( - { - await saveApiKey( apiKey ); - setIsExpanded( false ); - } } - /> - ) } - + ); } // Gemini connector render component -function GeminiConnector( { label, description }: ConnectorRenderProps ) { - const { - pluginStatus, - isExpanded, - setIsExpanded, - isBusy, - currentApiKey, - handleButtonClick, - getButtonLabel, - saveApiKey, - } = useConnectorPlugin( { - pluginSlug: 'ai-provider-for-google', - settingName: 'connectors_gemini_api_key', - } ); - +function GeminiConnector( props: ConnectorRenderProps ) { return ( - } - name={ label } - description={ description } - actionArea={ - - } - > - { isExpanded && pluginStatus === 'active' && ( - { - await saveApiKey( apiKey ); - setIsExpanded( false ); - } } - /> - ) } - + ); } diff --git a/routes/connections-home/use-connector-plugin.ts b/routes/connections-home/use-connector-plugin.ts index dfbad09ea31a55..8da7e789a733d1 100644 --- a/routes/connections-home/use-connector-plugin.ts +++ b/routes/connections-home/use-connector-plugin.ts @@ -17,10 +17,12 @@ interface UseConnectorPluginReturn { isExpanded: boolean; setIsExpanded: ( expanded: boolean ) => void; isBusy: boolean; + isConnected: boolean; currentApiKey: string; handleButtonClick: () => void; getButtonLabel: () => string; saveApiKey: ( apiKey: string ) => Promise< void >; + removeApiKey: () => Promise< void >; } export function useConnectorPlugin( { @@ -33,6 +35,8 @@ export function useConnectorPlugin( { const [ isBusy, setIsBusy ] = useState( false ); const [ currentApiKey, setCurrentApiKey ] = useState( '' ); + const isConnected = pluginStatus === 'active' && currentApiKey !== ''; + // Fetch the current API key const fetchApiKey = useCallback( async () => { try { @@ -128,6 +132,9 @@ export function useConnectorPlugin( { if ( isExpanded ) { return __( 'Cancel' ); } + if ( isConnected ) { + return __( 'Edit' ); + } switch ( pluginStatus ) { case 'checking': return __( 'Checking…' ); @@ -157,14 +164,34 @@ export function useConnectorPlugin( { } }; + const removeApiKey = async () => { + try { + await apiFetch( { + method: 'POST', + path: '/wp/v2/settings', + data: { + [ settingName ]: '', + }, + } ); + setCurrentApiKey( '' ); + setIsExpanded( false ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Failed to remove API key:', error ); + throw error; + } + }; + return { pluginStatus, isExpanded, setIsExpanded, isBusy, + isConnected, currentApiKey, handleButtonClick, getButtonLabel, saveApiKey, + removeApiKey, }; } From b3426bc46b3c92866ad0dbb1b8e3f1c638341aa4 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 14:11:12 +0000 Subject: [PATCH 30/81] Ui improvements take two --- routes/connections-home/default-connectors.tsx | 11 +++++------ routes/connections-home/use-connector-plugin.ts | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index a28b0bf19ffea5..218f5e1970e270 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -30,7 +30,7 @@ const ConnectedBadge = () => ( whiteSpace: 'nowrap', } } > - { __( 'Connected' ) } + { __( 'API key provided' ) } ); @@ -172,14 +172,12 @@ function ProviderConnector( { description={ description } actionArea={ - { isConnected && ! isExpanded && } + { isConnected && } diff --git a/routes/connections-home/use-connector-plugin.ts b/routes/connections-home/use-connector-plugin.ts index 14c4500ef6e084..b6c752808b3d25 100644 --- a/routes/connections-home/use-connector-plugin.ts +++ b/routes/connections-home/use-connector-plugin.ts @@ -149,14 +149,23 @@ export function useConnectorPlugin( { const saveApiKey = async ( apiKey: string ) => { try { - await apiFetch( { + const result = await apiFetch< Record< string, string > >( { method: 'POST', path: '/wp/v2/settings', data: { [ settingName ]: apiKey, }, } ); - setCurrentApiKey( apiKey ); + + // If we sent a non-empty key but the returned value didn't + // change, the server rejected the update (validation failed). + if ( apiKey && result[ settingName ] === currentApiKey ) { + throw new Error( + 'The API key could not be saved. The key may be invalid.' + ); + } + + setCurrentApiKey( result[ settingName ] || '' ); } catch ( error ) { // eslint-disable-next-line no-console console.error( 'Failed to save API key:', error ); From d4c0d477da8338ccb4d9519460071f73b57e0d53 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 18:06:35 +0000 Subject: [PATCH 38/81] add get level validation --- .../connectors/default-connectors.php | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 3e1df917d134db..84c52f8472c252 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -23,13 +23,35 @@ function gutenberg_mask_api_key( $key ) { * * @param string $option_name The option name to mask. */ -function gutenberg_add_api_key_mask_filter( $option_name ) { +function gutenberg_add_api_key_mask_filter( $option_name, $provider_id = '' ) { add_filter( "option_{$option_name}", - function ( $value ) { + function ( $value ) use ( $provider_id ) { if ( empty( $value ) ) { return $value; } + + if ( $provider_id ) { + try { + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + + if ( ! $registry->hasProvider( $provider_id ) ) { + return ''; + } + + $registry->setProviderRequestAuthentication( + $provider_id, + new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $value ) + ); + + if ( ! $registry->isProviderConfigured( $provider_id ) ) { + return ''; + } + } catch ( \Error $e ) { + // WP AI Client not available — skip validation, return masked. + } + } + return gutenberg_mask_api_key( $value ); } ); @@ -43,14 +65,14 @@ function ( $value ) { * @param string $option_name The option name for the API key. * @return string The real API key value. */ -function gutenberg_get_real_api_key( $option_name ) { +function gutenberg_get_real_api_key( $option_name, $provider_id = '' ) { // Remove all masking filters on this option. remove_all_filters( "option_{$option_name}" ); $value = get_option( $option_name, '' ); // Re-add the masking filter. - gutenberg_add_api_key_mask_filter( $option_name ); + gutenberg_add_api_key_mask_filter( $option_name, $provider_id ); return $value; } @@ -75,7 +97,7 @@ function gutenberg_register_connector_api_key_setting( $option_name, $provider_i ) ); - gutenberg_add_api_key_mask_filter( $option_name ); + gutenberg_add_api_key_mask_filter( $option_name, $provider_id ); if ( $provider_id ) { gutenberg_add_api_key_validation_filter( $option_name, $provider_id ); @@ -138,7 +160,7 @@ function ( $value, $old_value ) use ( $provider_id ) { * @param string $provider_id The WP AI client provider ID. */ function gutenberg_pass_connector_key_to_ai_client( $option_name, $provider_id ) { - $api_key = gutenberg_get_real_api_key( $option_name ); + $api_key = gutenberg_get_real_api_key( $option_name, $provider_id ); if ( empty( $api_key ) ) { return; From 733dceb2e9e027c05dacf31fbeb30733ab4b3f28 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 24 Feb 2026 18:37:09 +0000 Subject: [PATCH 39/81] fix read time validation --- .../connectors/default-connectors.php | 20 +++++++++---------- .../connections-home/default-connectors.tsx | 2 +- .../connections-home/use-connector-plugin.ts | 8 ++++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 84c52f8472c252..91553e3dbf7c4c 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -35,17 +35,15 @@ function ( $value ) use ( $provider_id ) { try { $registry = \WordPress\AiClient\AiClient::defaultRegistry(); - if ( ! $registry->hasProvider( $provider_id ) ) { - return ''; - } - - $registry->setProviderRequestAuthentication( - $provider_id, - new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $value ) - ); - - if ( ! $registry->isProviderConfigured( $provider_id ) ) { - return ''; + if ( $registry->hasProvider( $provider_id ) ) { + $registry->setProviderRequestAuthentication( + $provider_id, + new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $value ) + ); + + if ( ! $registry->isProviderConfigured( $provider_id ) ) { + return 'invalid_key'; + } } } catch ( \Error $e ) { // WP AI Client not available — skip validation, return masked. diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index eac5f88be4bad8..2622dbac746482 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -27,7 +27,7 @@ const ConnectedBadge = () => ( whiteSpace: 'nowrap', } } > - { __( 'API key provided' ) } + { __( 'Connected' ) } ); diff --git a/routes/connections-home/use-connector-plugin.ts b/routes/connections-home/use-connector-plugin.ts index b6c752808b3d25..4bf6d04629ce43 100644 --- a/routes/connections-home/use-connector-plugin.ts +++ b/routes/connections-home/use-connector-plugin.ts @@ -35,7 +35,10 @@ export function useConnectorPlugin( { const [ isBusy, setIsBusy ] = useState( false ); const [ currentApiKey, setCurrentApiKey ] = useState( '' ); - const isConnected = pluginStatus === 'active' && currentApiKey !== ''; + const isConnected = + pluginStatus === 'active' && + currentApiKey !== '' && + currentApiKey !== 'invalid_key'; // Fetch the current API key const fetchApiKey = useCallback( async () => { @@ -43,7 +46,8 @@ export function useConnectorPlugin( { const settings = await apiFetch< Record< string, string > >( { path: '/wp/v2/settings', } ); - setCurrentApiKey( settings[ settingName ] || '' ); + const key = settings[ settingName ] || ''; + setCurrentApiKey( key === 'invalid_key' ? '' : key ); } catch { // Ignore errors } From ab5de8d1809672d7541a8d42f960d5f4a369600f Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 25 Feb 2026 09:11:04 +0100 Subject: [PATCH 40/81] Fix PHPCS errors in connectors PHP files Co-Authored-By: Claude Opus 4.6 --- lib/experimental/connectors/debug-test.php | 2 +- lib/experimental/connectors/load.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/experimental/connectors/debug-test.php b/lib/experimental/connectors/debug-test.php index 283fc9a3fa370a..7757f18c0908e2 100644 --- a/lib/experimental/connectors/debug-test.php +++ b/lib/experimental/connectors/debug-test.php @@ -21,7 +21,7 @@ function gutenberg_test_ai_provider( $registry, $provider_id ) { ); try { - $response = wp_ai_client_prompt( 'Say the word I will pass' ) + $response = wp_ai_client_prompt( 'Say the word I will pass' ) ->with_text( 'hello' ) ->using_provider( $provider_id ) ->generate_text(); diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index cee5711fde3635..962934422d580a 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -49,4 +49,4 @@ function gutenberg_enqueue_connectors_extension( $hook_suffix ) { add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_connectors_extension' ); require __DIR__ . '/default-connectors.php'; -require __DIR__ . '/debug-test.php'; \ No newline at end of file +require __DIR__ . '/debug-test.php'; From 045d6241249ab1cf1468bb64a17cfa7ae209ff5c Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 25 Feb 2026 10:02:04 +0100 Subject: [PATCH 41/81] Add E2E tests for Connectors page and BEM class names for provider cards Add className prop to ConnectorItem, used by ProviderConnector to generate BEM classes from pluginSlug (e.g. connector-item--ai-provider-for-openai). Add E2E tests verifying the Settings menu link and default provider cards. --- packages/connectors/src/connector-item.tsx | 4 +- .../connections-home/default-connectors.tsx | 1 + test/e2e/specs/admin/connections.spec.js | 103 ++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 test/e2e/specs/admin/connections.spec.js diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 91334e27eabeb7..2975857720223d 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -20,6 +20,7 @@ import { __ } from '@wordpress/i18n'; import type { ReactNode } from 'react'; export interface ConnectorItemProps { + className?: string; icon?: ReactNode; name: string; description: string; @@ -28,6 +29,7 @@ export interface ConnectorItemProps { } export function ConnectorItem( { + className, icon, name, description, @@ -35,7 +37,7 @@ export function ConnectorItem( { children, }: ConnectorItemProps ) { return ( - + { icon } diff --git a/routes/connections-home/default-connectors.tsx b/routes/connections-home/default-connectors.tsx index 2622dbac746482..15916b9f72a430 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connections-home/default-connectors.tsx @@ -230,6 +230,7 @@ function ProviderConnector( { return ( } name={ label } description={ description } diff --git a/test/e2e/specs/admin/connections.spec.js b/test/e2e/specs/admin/connections.spec.js new file mode 100644 index 00000000000000..ebe1802f0d7752 --- /dev/null +++ b/test/e2e/specs/admin/connections.spec.js @@ -0,0 +1,103 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +const SETTINGS_PAGE_PATH = 'options-general.php'; +const CONNECTORS_PAGE_QUERY = 'page=connections-wp-admin'; + +test.describe( 'Connections', () => { + test( 'should show a Connectors link in the Settings menu', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( SETTINGS_PAGE_PATH ); + + const settingsMenu = page.locator( '#menu-settings' ); + const connectorsLink = settingsMenu.getByRole( 'link', { + name: 'Connectors', + } ); + await expect( connectorsLink ).toBeVisible(); + await expect( connectorsLink ).toHaveAttribute( + 'href', + `${ SETTINGS_PAGE_PATH }?${ CONNECTORS_PAGE_QUERY }` + ); + } ); + + test.describe( 'Connectors page', () => { + test.beforeEach( async ( { admin } ) => { + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONNECTORS_PAGE_QUERY + ); + } ); + + test( 'should display default providers with install buttons', async ( { + page, + } ) => { + // Verify the page title is visible. + await expect( + page.getByRole( 'heading', { name: 'Connectors' } ) + ).toBeVisible(); + + // Verify each connector card allows installing the plugin. + const openaiCard = page.locator( + '.connector-item--ai-provider-for-openai' + ); + await expect( openaiCard ).toBeVisible(); + await expect( + openaiCard.getByText( 'OpenAI', { exact: true } ) + ).toBeVisible(); + await expect( + openaiCard.getByText( + 'Text, image, and code generation with GPT and DALL-E.' + ) + ).toBeVisible(); + await expect( + openaiCard.getByRole( 'button', { name: 'Install' } ) + ).toBeVisible(); + + const claudeCard = page.locator( + '.connector-item--ai-provider-for-anthropic' + ); + await expect( claudeCard ).toBeVisible(); + await expect( + claudeCard.getByText( 'Claude', { exact: true } ) + ).toBeVisible(); + await expect( + claudeCard.getByText( + 'Writing, research, and analysis with Claude.' + ) + ).toBeVisible(); + await expect( + claudeCard.getByRole( 'button', { name: 'Install' } ) + ).toBeVisible(); + + const geminiCard = page.locator( + '.connector-item--ai-provider-for-google' + ); + await expect( geminiCard ).toBeVisible(); + await expect( + geminiCard.getByText( 'Gemini', { exact: true } ) + ).toBeVisible(); + await expect( + geminiCard.getByText( + "Content generation, translation, and vision with Google's Gemini." + ) + ).toBeVisible(); + await expect( + geminiCard.getByRole( 'button', { name: 'Install' } ) + ).toBeVisible(); + + // Verify the plugin directory search link is present. + await expect( + page.getByRole( 'link', { + name: 'search the plugin directory', + } ) + ).toHaveAttribute( + 'href', + 'plugin-install.php?s=connector&tab=search&type=tag' + ); + } ); + } ); +} ); From d4285b478421e40c0e0762d16927226783f0ab0d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 25 Feb 2026 10:57:55 +0100 Subject: [PATCH 42/81] Rename "connections" to "connectors" for consistency The UI labels already used "Connectors" but route directories, CSS classes, page identifiers, the wp-admin menu slug, and callback names still used "connections". This aligns all references under "connectors". Co-Authored-By: Claude Opus 4.6 --- lib/experimental/connectors/load.php | 6 +++--- package-lock.json | 12 ++++++------ package.json | 2 +- .../default-connectors.tsx | 12 +++++++++--- .../package.json | 4 ++-- .../{connections-home => connectors-home}/route.ts | 2 +- .../{connections-home => connectors-home}/stage.tsx | 2 +- .../{connections-home => connectors-home}/style.scss | 2 +- .../use-connector-plugin.ts | 0 .../{connections.spec.js => connectors.spec.js} | 4 ++-- 10 files changed, 26 insertions(+), 20 deletions(-) rename routes/{connections-home => connectors-home}/default-connectors.tsx (97%) rename routes/{connections-home => connectors-home}/package.json (89%) rename routes/{connections-home => connectors-home}/route.ts (73%) rename routes/{connections-home => connectors-home}/stage.tsx (97%) rename routes/{connections-home => connectors-home}/style.scss (93%) rename routes/{connections-home => connectors-home}/use-connector-plugin.ts (100%) rename test/e2e/specs/admin/{connections.spec.js => connectors.spec.js} (96%) diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index 962934422d580a..a80e02d456c19f 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -16,8 +16,8 @@ function gutenberg_connectors_add_settings_menu_item() { __( 'Connectors', 'gutenberg' ), __( 'Connectors', 'gutenberg' ), 'manage_options', - 'connections-wp-admin', - 'gutenberg_connections_wp_admin_render_page', + 'connectors-wp-admin', + 'gutenberg_connectors_wp_admin_render_page', 1 ); } @@ -41,7 +41,7 @@ function gutenberg_register_connectors_extension_module() { * @param string $hook_suffix The current admin page. */ function gutenberg_enqueue_connectors_extension( $hook_suffix ) { - if ( 'settings_page_connections-wp-admin' !== $hook_suffix ) { + if ( 'settings_page_connectors-wp-admin' !== $hook_suffix ) { return; } wp_enqueue_script_module( 'gutenberg/connectors-extension' ); diff --git a/package-lock.json b/package-lock.json index 19ae1a1256d4f3..963cf60fbc3855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20767,14 +20767,14 @@ "resolved": "packages/compose", "link": true }, - "node_modules/@wordpress/connections-home-route": { - "resolved": "routes/connections-home", - "link": true - }, "node_modules/@wordpress/connectors": { "resolved": "packages/connectors", "link": true }, + "node_modules/@wordpress/connectors-home-route": { + "resolved": "routes/connectors-home", + "link": true + }, "node_modules/@wordpress/core-abilities": { "resolved": "packages/core-abilities", "link": true @@ -62987,8 +62987,8 @@ } } }, - "routes/connections-home": { - "name": "@wordpress/connections-home-route", + "routes/connectors-home": { + "name": "@wordpress/connectors-home-route", "version": "1.0.0", "dependencies": { "@wordpress/admin-ui": "file:../../packages/admin-ui", diff --git a/package.json b/package.json index 490539c8c5439e..eb94b5a35f43c4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ ] }, "font-library", - "connections" + "connectors" ] }, "config": { diff --git a/routes/connections-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx similarity index 97% rename from routes/connections-home/default-connectors.tsx rename to routes/connectors-home/default-connectors.tsx index 15916b9f72a430..1a909b05aa886d 100644 --- a/routes/connections-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -151,14 +151,18 @@ function createKeyValidator( providerName: string, rule: KeyValidationRule ) { return rule.exactLength ? sprintf( /* translators: 1: provider name, 2: key prefix, 3: number of characters */ - __( '%1$s API keys start with "%2$s" and are %3$d characters long.' ), + __( + '%1$s API keys start with "%2$s" and are %3$d characters long.' + ), providerName, rule.prefix, rule.exactLength ) : sprintf( /* translators: 1: provider name, 2: key prefix, 3: minimum number of characters */ - __( '%1$s API keys start with "%2$s" and are at least %3$d characters long.' ), + __( + '%1$s API keys start with "%2$s" and are at least %3$d characters long.' + ), providerName, rule.prefix, rule.minLength @@ -174,7 +178,9 @@ function createKeyValidator( providerName: string, rule: KeyValidationRule ) { ) : sprintf( /* translators: 1: provider name, 2: minimum number of characters */ - __( '%1$s API keys are at least %2$d characters long.' ), + __( + '%1$s API keys are at least %2$d characters long.' + ), providerName, rule.minLength ); diff --git a/routes/connections-home/package.json b/routes/connectors-home/package.json similarity index 89% rename from routes/connections-home/package.json rename to routes/connectors-home/package.json index 64a8c0c1cea73a..862f5f2b3ce8c5 100644 --- a/routes/connections-home/package.json +++ b/routes/connectors-home/package.json @@ -1,11 +1,11 @@ { - "name": "@wordpress/connections-home-route", + "name": "@wordpress/connectors-home-route", "version": "1.0.0", "private": true, "route": { "path": "/", "page": [ - "connections" + "connectors" ] }, "dependencies": { diff --git a/routes/connections-home/route.ts b/routes/connectors-home/route.ts similarity index 73% rename from routes/connections-home/route.ts rename to routes/connectors-home/route.ts index 3094ea51bd2535..e8de335016b8ac 100644 --- a/routes/connections-home/route.ts +++ b/routes/connectors-home/route.ts @@ -4,5 +4,5 @@ import { __ } from '@wordpress/i18n'; export const route = { - title: () => __( 'Connections' ), + title: () => __( 'Connectors' ), }; diff --git a/routes/connections-home/stage.tsx b/routes/connectors-home/stage.tsx similarity index 97% rename from routes/connections-home/stage.tsx rename to routes/connectors-home/stage.tsx index 218eff8c5fe558..3c00d79cd08245 100644 --- a/routes/connections-home/stage.tsx +++ b/routes/connectors-home/stage.tsx @@ -30,7 +30,7 @@ function ConnectorsPage() { 'All of your API keys and credentials are stored here and shared across plugins. Configure once and use everywhere.' ) } > -
+
{ connectors.map( ( connector: ConnectorConfig ) => { if ( connector.render ) { diff --git a/routes/connections-home/style.scss b/routes/connectors-home/style.scss similarity index 93% rename from routes/connections-home/style.scss rename to routes/connectors-home/style.scss index f8b25a0adf9384..01d92ccfc1e51d 100644 --- a/routes/connections-home/style.scss +++ b/routes/connectors-home/style.scss @@ -1,4 +1,4 @@ -.connections-page { +.connectors-page { width: 100%; max-width: 800px; margin: 0 auto; diff --git a/routes/connections-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts similarity index 100% rename from routes/connections-home/use-connector-plugin.ts rename to routes/connectors-home/use-connector-plugin.ts diff --git a/test/e2e/specs/admin/connections.spec.js b/test/e2e/specs/admin/connectors.spec.js similarity index 96% rename from test/e2e/specs/admin/connections.spec.js rename to test/e2e/specs/admin/connectors.spec.js index ebe1802f0d7752..e3b008023967bb 100644 --- a/test/e2e/specs/admin/connections.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -4,9 +4,9 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); const SETTINGS_PAGE_PATH = 'options-general.php'; -const CONNECTORS_PAGE_QUERY = 'page=connections-wp-admin'; +const CONNECTORS_PAGE_QUERY = 'page=connectors-wp-admin'; -test.describe( 'Connections', () => { +test.describe( 'Connectors', () => { test( 'should show a Connectors link in the Settings menu', async ( { page, admin, From 0349c3455b46c41e1300ad14b57505dd41e44a6a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 10:27:09 +0000 Subject: [PATCH 43/81] fix await --- routes/connectors-home/use-connector-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts index 4bf6d04629ce43..32bb97a60bfd2a 100644 --- a/routes/connectors-home/use-connector-plugin.ts +++ b/routes/connectors-home/use-connector-plugin.ts @@ -70,8 +70,8 @@ export function useConnectorPlugin( { if ( ! plugin ) { setPluginStatus( 'not-installed' ); } else if ( plugin.status === 'active' ) { + await fetchApiKey(); setPluginStatus( 'active' ); - fetchApiKey(); } else { setPluginStatus( 'inactive' ); } From 9ba27a5485c877d6739285a5efff545b12626570 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 10:46:49 +0000 Subject: [PATCH 44/81] don't include mechanism if AI client is not available --- lib/experimental/connectors/default-connectors.php | 6 ++++++ lib/experimental/connectors/load.php | 3 +++ 2 files changed, 9 insertions(+) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 91553e3dbf7c4c..b7b8b8080d24da 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -185,6 +185,9 @@ function gutenberg_pass_connector_key_to_ai_client( $option_name, $provider_id ) * Registers the default connector settings. */ function gutenberg_register_default_connector_settings() { + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return; + } gutenberg_register_connector_api_key_setting( 'connectors_gemini_api_key', 'google' ); gutenberg_register_connector_api_key_setting( 'connectors_openai_api_key', 'openai' ); gutenberg_register_connector_api_key_setting( 'connectors_anthropic_api_key', 'anthropic' ); @@ -195,6 +198,9 @@ function gutenberg_register_default_connector_settings() { * Passes the default connector API keys to the WP AI client. */ function gutenberg_pass_default_connector_keys_to_ai_client() { + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return; + } gutenberg_pass_connector_key_to_ai_client( 'connectors_gemini_api_key', 'google' ); gutenberg_pass_connector_key_to_ai_client( 'connectors_openai_api_key', 'openai' ); gutenberg_pass_connector_key_to_ai_client( 'connectors_anthropic_api_key', 'anthropic' ); diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index a80e02d456c19f..4b7154cf748da1 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -11,6 +11,9 @@ * Registers the Connectors menu item under Settings. */ function gutenberg_connectors_add_settings_menu_item() { + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return; + } add_submenu_page( 'options-general.php', __( 'Connectors', 'gutenberg' ), From b94264cc08b08ec5f85bf245bcb43adb443bbe5b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 11:16:07 +0000 Subject: [PATCH 45/81] avoid anonymous functions --- .../connectors/default-connectors.php | 302 ++++++++++-------- 1 file changed, 171 insertions(+), 131 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index b7b8b8080d24da..40e5385cb7325c 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -19,40 +19,52 @@ function gutenberg_mask_api_key( $key ) { } /** - * Filters get_option to return a masked API key for a connector setting. + * Checks whether an API key is valid for a given provider. * - * @param string $option_name The option name to mask. + * @param string $key The API key to check. + * @param string $provider_id The WP AI client provider ID. + * @return bool|null True if valid, false if invalid, null if unable to determine. */ -function gutenberg_add_api_key_mask_filter( $option_name, $provider_id = '' ) { - add_filter( - "option_{$option_name}", - function ( $value ) use ( $provider_id ) { - if ( empty( $value ) ) { - return $value; - } - - if ( $provider_id ) { - try { - $registry = \WordPress\AiClient\AiClient::defaultRegistry(); - - if ( $registry->hasProvider( $provider_id ) ) { - $registry->setProviderRequestAuthentication( - $provider_id, - new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $value ) - ); - - if ( ! $registry->isProviderConfigured( $provider_id ) ) { - return 'invalid_key'; - } - } - } catch ( \Error $e ) { - // WP AI Client not available — skip validation, return masked. - } - } - - return gutenberg_mask_api_key( $value ); +function gutenberg_is_api_key_valid( $key, $provider_id ) { + try { + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + + if ( ! $registry->hasProvider( $provider_id ) ) { + return null; } - ); + + $registry->setProviderRequestAuthentication( + $provider_id, + new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $key ) + ); + + return $registry->isProviderConfigured( $provider_id ); + } catch ( \Error $e ) { + return null; + } +} + +/** + * Sets the API key authentication for a provider on the WP AI Client registry. + * + * @param string $key The API key. + * @param string $provider_id The WP AI client provider ID. + */ +function gutenberg_set_provider_api_key( $key, $provider_id ) { + try { + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + + if ( ! $registry->hasProvider( $provider_id ) ) { + return; + } + + $registry->setProviderRequestAuthentication( + $provider_id, + new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $key ) + ); + } catch ( \Error $e ) { + // WP AI Client not available. + } } /** @@ -60,137 +72,155 @@ function ( $value ) use ( $provider_id ) { * * Temporarily removes the masking filter, reads the option, then re-adds it. * - * @param string $option_name The option name for the API key. + * @param string $option_name The option name for the API key. + * @param string $mask_callback The mask filter function name. * @return string The real API key value. */ -function gutenberg_get_real_api_key( $option_name, $provider_id = '' ) { - // Remove all masking filters on this option. - remove_all_filters( "option_{$option_name}" ); - +function gutenberg_get_real_api_key( $option_name, $mask_callback ) { + remove_filter( "option_{$option_name}", $mask_callback ); $value = get_option( $option_name, '' ); - - // Re-add the masking filter. - gutenberg_add_api_key_mask_filter( $option_name, $provider_id ); - + add_filter( "option_{$option_name}", $mask_callback ); return $value; } +// --- Gemini (Google) --- + /** - * Registers a connector API key setting and adds masking and validation filters. + * Masks and validates the Gemini API key on read. * - * Base function that can be used by any provider. - * - * @param string $option_name The option name for the API key. - * @param string $provider_id Optional. The WP AI client provider ID for validation. + * @param string $value The raw option value. + * @return string Masked key, 'invalid_key', or empty string. */ -function gutenberg_register_connector_api_key_setting( $option_name, $provider_id = '' ) { - register_setting( - 'connectors', - $option_name, - array( - 'type' => 'string', - 'default' => '', - 'show_in_rest' => true, - 'sanitize_callback' => 'sanitize_text_field', - ) - ); - - gutenberg_add_api_key_mask_filter( $option_name, $provider_id ); - - if ( $provider_id ) { - gutenberg_add_api_key_validation_filter( $option_name, $provider_id ); +function gutenberg_mask_gemini_api_key( $value ) { + if ( empty( $value ) ) { + return $value; + } + if ( false === gutenberg_is_api_key_valid( $value, 'google' ) ) { + return 'invalid_key'; } + return gutenberg_mask_api_key( $value ); } /** - * Adds a pre_update_option filter that validates an API key against the WP AI Client - * before allowing it to be persisted. - * - * If the key is invalid (the provider cannot be configured with it), the filter - * returns the old value, effectively rejecting the update. The client detects - * the unchanged response and surfaces an error. + * Validates the Gemini API key before saving. * - * @param string $option_name The option name for the API key. - * @param string $provider_id The WP AI client provider ID. + * @param string $value The new value. + * @param string $old_value The previous value. + * @return string The value to persist. */ -function gutenberg_add_api_key_validation_filter( $option_name, $provider_id ) { - add_filter( - "pre_update_option_{$option_name}", - function ( $value, $old_value ) use ( $provider_id ) { - // Always allow clearing the key. - if ( empty( $value ) ) { - return $value; - } - - try { - $registry = \WordPress\AiClient\AiClient::defaultRegistry(); - - if ( ! $registry->hasProvider( $provider_id ) ) { - return $old_value; - } - - $registry->setProviderRequestAuthentication( - $provider_id, - new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $value ) - ); - - if ( ! $registry->isProviderConfigured( $provider_id ) ) { - return $old_value; - } - } catch ( \Error $e ) { - // WP AI Client not available — allow update. - return $value; - } - - return $value; - }, - 10, - 2 - ); +function gutenberg_validate_gemini_api_key_on_save( $value, $old_value ) { + if ( empty( $value ) ) { + return $value; + } + $valid = gutenberg_is_api_key_valid( $value, 'google' ); + return false === $valid ? $old_value : $value; } +// --- OpenAI --- + /** - * Passes a connector API key to the WP AI client. - * - * Base function that can be used by any provider. + * Masks and validates the OpenAI API key on read. * - * @param string $option_name The option name for the API key. - * @param string $provider_id The WP AI client provider ID. + * @param string $value The raw option value. + * @return string Masked key, 'invalid_key', or empty string. */ -function gutenberg_pass_connector_key_to_ai_client( $option_name, $provider_id ) { - $api_key = gutenberg_get_real_api_key( $option_name, $provider_id ); +function gutenberg_mask_openai_api_key( $value ) { + if ( empty( $value ) ) { + return $value; + } + if ( false === gutenberg_is_api_key_valid( $value, 'openai' ) ) { + return 'invalid_key'; + } + return gutenberg_mask_api_key( $value ); +} - if ( empty( $api_key ) ) { - return; +/** + * Validates the OpenAI API key before saving. + * + * @param string $value The new value. + * @param string $old_value The previous value. + * @return string The value to persist. + */ +function gutenberg_validate_openai_api_key_on_save( $value, $old_value ) { + if ( empty( $value ) ) { + return $value; } + $valid = gutenberg_is_api_key_valid( $value, 'openai' ); + return false === $valid ? $old_value : $value; +} - try { - $registry = \WordPress\AiClient\AiClient::defaultRegistry(); +// --- Anthropic --- - if ( ! $registry->hasProvider( $provider_id ) ) { - return; - } +/** + * Masks and validates the Anthropic API key on read. + * + * @param string $value The raw option value. + * @return string Masked key, 'invalid_key', or empty string. + */ +function gutenberg_mask_anthropic_api_key( $value ) { + if ( empty( $value ) ) { + return $value; + } + if ( false === gutenberg_is_api_key_valid( $value, 'anthropic' ) ) { + return 'invalid_key'; + } + return gutenberg_mask_api_key( $value ); +} - $registry->setProviderRequestAuthentication( - $provider_id, - new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $api_key ) - ); - } catch ( \Error $e ) { - // WP AI Client not available. - return; +/** + * Validates the Anthropic API key before saving. + * + * @param string $value The new value. + * @param string $old_value The previous value. + * @return string The value to persist. + */ +function gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { + if ( empty( $value ) ) { + return $value; } + $valid = gutenberg_is_api_key_valid( $value, 'anthropic' ); + return false === $valid ? $old_value : $value; } +// --- Registration --- + /** - * Registers the default connector settings. + * Registers the default connector settings, mask filters, and validation filters. */ function gutenberg_register_default_connector_settings() { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } - gutenberg_register_connector_api_key_setting( 'connectors_gemini_api_key', 'google' ); - gutenberg_register_connector_api_key_setting( 'connectors_openai_api_key', 'openai' ); - gutenberg_register_connector_api_key_setting( 'connectors_anthropic_api_key', 'anthropic' ); + + $connectors = array( + 'connectors_gemini_api_key' => array( + 'mask' => 'gutenberg_mask_gemini_api_key', + 'validate' => 'gutenberg_validate_gemini_api_key_on_save', + ), + 'connectors_openai_api_key' => array( + 'mask' => 'gutenberg_mask_openai_api_key', + 'validate' => 'gutenberg_validate_openai_api_key_on_save', + ), + 'connectors_anthropic_api_key' => array( + 'mask' => 'gutenberg_mask_anthropic_api_key', + 'validate' => 'gutenberg_validate_anthropic_api_key_on_save', + ), + ); + + foreach ( $connectors as $option_name => $callbacks ) { + register_setting( + 'connectors', + $option_name, + array( + 'type' => 'string', + 'default' => '', + 'show_in_rest' => true, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + add_filter( "option_{$option_name}", $callbacks['mask'] ); + add_filter( "pre_update_option_{$option_name}", $callbacks['validate'], 10, 2 ); + } } add_action( 'init', 'gutenberg_register_default_connector_settings' ); @@ -201,8 +231,18 @@ function gutenberg_pass_default_connector_keys_to_ai_client() { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } - gutenberg_pass_connector_key_to_ai_client( 'connectors_gemini_api_key', 'google' ); - gutenberg_pass_connector_key_to_ai_client( 'connectors_openai_api_key', 'openai' ); - gutenberg_pass_connector_key_to_ai_client( 'connectors_anthropic_api_key', 'anthropic' ); + + $connectors = array( + 'connectors_gemini_api_key' => array( 'google', 'gutenberg_mask_gemini_api_key' ), + 'connectors_openai_api_key' => array( 'openai', 'gutenberg_mask_openai_api_key' ), + 'connectors_anthropic_api_key' => array( 'anthropic', 'gutenberg_mask_anthropic_api_key' ), + ); + + foreach ( $connectors as $option_name => $config ) { + $api_key = gutenberg_get_real_api_key( $option_name, $config[1] ); + if ( ! empty( $api_key ) ) { + gutenberg_set_provider_api_key( $api_key, $config[0] ); + } + } } add_action( 'init', 'gutenberg_pass_default_connector_keys_to_ai_client' ); From d219ba3d4203a789f4da05ae4af5ecd974e7b70e Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 11:47:00 +0000 Subject: [PATCH 46/81] mark functions as private --- .../connectors/default-connectors.php | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 40e5385cb7325c..6fe13a0f530ec0 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -8,10 +8,12 @@ /** * Masks an API key, showing only the last 4 characters. * + * @access private + * * @param string $key The API key to mask. * @return string The masked key, e.g. "••••••••••••fj39". */ -function gutenberg_mask_api_key( $key ) { +function _gutenberg_mask_api_key( $key ) { if ( strlen( $key ) <= 4 ) { return $key; } @@ -21,11 +23,13 @@ function gutenberg_mask_api_key( $key ) { /** * Checks whether an API key is valid for a given provider. * + * @access private + * * @param string $key The API key to check. * @param string $provider_id The WP AI client provider ID. * @return bool|null True if valid, false if invalid, null if unable to determine. */ -function gutenberg_is_api_key_valid( $key, $provider_id ) { +function _gutenberg_is_api_key_valid( $key, $provider_id ) { try { $registry = \WordPress\AiClient\AiClient::defaultRegistry(); @@ -47,10 +51,12 @@ function gutenberg_is_api_key_valid( $key, $provider_id ) { /** * Sets the API key authentication for a provider on the WP AI Client registry. * + * @access private + * * @param string $key The API key. * @param string $provider_id The WP AI client provider ID. */ -function gutenberg_set_provider_api_key( $key, $provider_id ) { +function _gutenberg_set_provider_api_key( $key, $provider_id ) { try { $registry = \WordPress\AiClient\AiClient::defaultRegistry(); @@ -72,11 +78,13 @@ function gutenberg_set_provider_api_key( $key, $provider_id ) { * * Temporarily removes the masking filter, reads the option, then re-adds it. * + * @access private + * * @param string $option_name The option name for the API key. * @param string $mask_callback The mask filter function name. * @return string The real API key value. */ -function gutenberg_get_real_api_key( $option_name, $mask_callback ) { +function _gutenberg_get_real_api_key( $option_name, $mask_callback ) { remove_filter( "option_{$option_name}", $mask_callback ); $value = get_option( $option_name, '' ); add_filter( "option_{$option_name}", $mask_callback ); @@ -88,31 +96,35 @@ function gutenberg_get_real_api_key( $option_name, $mask_callback ) { /** * Masks and validates the Gemini API key on read. * + * @access private + * * @param string $value The raw option value. * @return string Masked key, 'invalid_key', or empty string. */ -function gutenberg_mask_gemini_api_key( $value ) { +function _gutenberg_mask_gemini_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === gutenberg_is_api_key_valid( $value, 'google' ) ) { + if ( false === _gutenberg_is_api_key_valid( $value, 'google' ) ) { return 'invalid_key'; } - return gutenberg_mask_api_key( $value ); + return _gutenberg_mask_api_key( $value ); } /** * Validates the Gemini API key before saving. * + * @access private + * * @param string $value The new value. * @param string $old_value The previous value. * @return string The value to persist. */ -function gutenberg_validate_gemini_api_key_on_save( $value, $old_value ) { +function _gutenberg_validate_gemini_api_key_on_save( $value, $old_value ) { if ( empty( $value ) ) { return $value; } - $valid = gutenberg_is_api_key_valid( $value, 'google' ); + $valid = _gutenberg_is_api_key_valid( $value, 'google' ); return false === $valid ? $old_value : $value; } @@ -121,31 +133,35 @@ function gutenberg_validate_gemini_api_key_on_save( $value, $old_value ) { /** * Masks and validates the OpenAI API key on read. * + * @access private + * * @param string $value The raw option value. * @return string Masked key, 'invalid_key', or empty string. */ -function gutenberg_mask_openai_api_key( $value ) { +function _gutenberg_mask_openai_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === gutenberg_is_api_key_valid( $value, 'openai' ) ) { + if ( false === _gutenberg_is_api_key_valid( $value, 'openai' ) ) { return 'invalid_key'; } - return gutenberg_mask_api_key( $value ); + return _gutenberg_mask_api_key( $value ); } /** * Validates the OpenAI API key before saving. * + * @access private + * * @param string $value The new value. * @param string $old_value The previous value. * @return string The value to persist. */ -function gutenberg_validate_openai_api_key_on_save( $value, $old_value ) { +function _gutenberg_validate_openai_api_key_on_save( $value, $old_value ) { if ( empty( $value ) ) { return $value; } - $valid = gutenberg_is_api_key_valid( $value, 'openai' ); + $valid = _gutenberg_is_api_key_valid( $value, 'openai' ); return false === $valid ? $old_value : $value; } @@ -154,31 +170,35 @@ function gutenberg_validate_openai_api_key_on_save( $value, $old_value ) { /** * Masks and validates the Anthropic API key on read. * + * @access private + * * @param string $value The raw option value. * @return string Masked key, 'invalid_key', or empty string. */ -function gutenberg_mask_anthropic_api_key( $value ) { +function _gutenberg_mask_anthropic_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === gutenberg_is_api_key_valid( $value, 'anthropic' ) ) { + if ( false === _gutenberg_is_api_key_valid( $value, 'anthropic' ) ) { return 'invalid_key'; } - return gutenberg_mask_api_key( $value ); + return _gutenberg_mask_api_key( $value ); } /** * Validates the Anthropic API key before saving. * + * @access private + * * @param string $value The new value. * @param string $old_value The previous value. * @return string The value to persist. */ -function gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { +function _gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { if ( empty( $value ) ) { return $value; } - $valid = gutenberg_is_api_key_valid( $value, 'anthropic' ); + $valid = _gutenberg_is_api_key_valid( $value, 'anthropic' ); return false === $valid ? $old_value : $value; } @@ -186,24 +206,26 @@ function gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { /** * Registers the default connector settings, mask filters, and validation filters. + * + * @access private */ -function gutenberg_register_default_connector_settings() { +function _gutenberg_register_default_connector_settings() { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } $connectors = array( 'connectors_gemini_api_key' => array( - 'mask' => 'gutenberg_mask_gemini_api_key', - 'validate' => 'gutenberg_validate_gemini_api_key_on_save', + 'mask' => '_gutenberg_mask_gemini_api_key', + 'validate' => '_gutenberg_validate_gemini_api_key_on_save', ), 'connectors_openai_api_key' => array( - 'mask' => 'gutenberg_mask_openai_api_key', - 'validate' => 'gutenberg_validate_openai_api_key_on_save', + 'mask' => '_gutenberg_mask_openai_api_key', + 'validate' => '_gutenberg_validate_openai_api_key_on_save', ), 'connectors_anthropic_api_key' => array( - 'mask' => 'gutenberg_mask_anthropic_api_key', - 'validate' => 'gutenberg_validate_anthropic_api_key_on_save', + 'mask' => '_gutenberg_mask_anthropic_api_key', + 'validate' => '_gutenberg_validate_anthropic_api_key_on_save', ), ); @@ -222,27 +244,29 @@ function gutenberg_register_default_connector_settings() { add_filter( "pre_update_option_{$option_name}", $callbacks['validate'], 10, 2 ); } } -add_action( 'init', 'gutenberg_register_default_connector_settings' ); +add_action( 'init', '_gutenberg_register_default_connector_settings' ); /** * Passes the default connector API keys to the WP AI client. + * + * @access private */ -function gutenberg_pass_default_connector_keys_to_ai_client() { +function _gutenberg_pass_default_connector_keys_to_ai_client() { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } $connectors = array( - 'connectors_gemini_api_key' => array( 'google', 'gutenberg_mask_gemini_api_key' ), - 'connectors_openai_api_key' => array( 'openai', 'gutenberg_mask_openai_api_key' ), - 'connectors_anthropic_api_key' => array( 'anthropic', 'gutenberg_mask_anthropic_api_key' ), + 'connectors_gemini_api_key' => array( 'google', '_gutenberg_mask_gemini_api_key' ), + 'connectors_openai_api_key' => array( 'openai', '_gutenberg_mask_openai_api_key' ), + 'connectors_anthropic_api_key' => array( 'anthropic', '_gutenberg_mask_anthropic_api_key' ), ); foreach ( $connectors as $option_name => $config ) { - $api_key = gutenberg_get_real_api_key( $option_name, $config[1] ); + $api_key = _gutenberg_get_real_api_key( $option_name, $config[1] ); if ( ! empty( $api_key ) ) { - gutenberg_set_provider_api_key( $api_key, $config[0] ); + _gutenberg_set_provider_api_key( $api_key, $config[0] ); } } } -add_action( 'init', 'gutenberg_pass_default_connector_keys_to_ai_client' ); +add_action( 'init', '_gutenberg_pass_default_connector_keys_to_ai_client' ); From 61020891c2014e8f7e2a44c3c57932dde62e368b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:04:56 +0000 Subject: [PATCH 47/81] Added shortcircuit on option to avoid validation keys on every get request --- .../connectors/default-connectors.php | 41 +++++++++++++++++++ .../connectors-home/use-connector-plugin.ts | 6 +-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 6fe13a0f530ec0..20ef6abb4dd112 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -202,6 +202,47 @@ function _gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { return false === $valid ? $old_value : $value; } +// --- REST API filtering --- + +/** + * Short-circuits get_option for connector API key settings that were not + * explicitly requested via _fields on the REST settings endpoint. + * + * This prevents the mask filter (and its expensive isProviderConfigured + * validation) from running for connector settings that the caller did + * not ask for. + * + * @access private + * + * @param mixed $response Result to send to the client. Usually null. + * @param WP_REST_Server $server The server instance. + * @param WP_REST_Request $request The request object. + * @return mixed Unmodified $response. + */ +function _gutenberg_skip_unrequested_connector_validation( $response, $server, $request ) { + if ( '/wp/v2/settings' !== $request->get_route() ) { + return $response; + } + + $fields = $request->get_param( '_fields' ); + $requested = $fields ? array_map( 'trim', explode( ',', $fields ) ) : array(); + + $connector_options = array( + 'connectors_gemini_api_key', + 'connectors_openai_api_key', + 'connectors_anthropic_api_key', + ); + + foreach ( $connector_options as $option_name ) { + if ( ! in_array( $option_name, $requested, true ) ) { + add_filter( "pre_option_{$option_name}", '__return_empty_string' ); + } + } + + return $response; +} +add_filter( 'rest_pre_dispatch', '_gutenberg_skip_unrequested_connector_validation', 10, 3 ); + // --- Registration --- /** diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts index 32bb97a60bfd2a..9ab1da78654931 100644 --- a/routes/connectors-home/use-connector-plugin.ts +++ b/routes/connectors-home/use-connector-plugin.ts @@ -44,7 +44,7 @@ export function useConnectorPlugin( { const fetchApiKey = useCallback( async () => { try { const settings = await apiFetch< Record< string, string > >( { - path: '/wp/v2/settings', + path: `/wp/v2/settings?_fields=${ settingName }`, } ); const key = settings[ settingName ] || ''; setCurrentApiKey( key === 'invalid_key' ? '' : key ); @@ -155,7 +155,7 @@ export function useConnectorPlugin( { try { const result = await apiFetch< Record< string, string > >( { method: 'POST', - path: '/wp/v2/settings', + path: `/wp/v2/settings?_fields=${ settingName }`, data: { [ settingName ]: apiKey, }, @@ -181,7 +181,7 @@ export function useConnectorPlugin( { try { await apiFetch( { method: 'POST', - path: '/wp/v2/settings', + path: `/wp/v2/settings?_fields=${ settingName }`, data: { [ settingName ]: '', }, From 3bec2bea2eb193a53081858b0d6055d394744790 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:18:09 +0000 Subject: [PATCH 48/81] Converservtive validation --- .../connectors/default-connectors.php | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 20ef6abb4dd112..37940c05bb1cdc 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -94,20 +94,17 @@ function _gutenberg_get_real_api_key( $option_name, $mask_callback ) { // --- Gemini (Google) --- /** - * Masks and validates the Gemini API key on read. + * Masks the Gemini API key on read. * * @access private * * @param string $value The raw option value. - * @return string Masked key, 'invalid_key', or empty string. + * @return string Masked key or empty string. */ function _gutenberg_mask_gemini_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === _gutenberg_is_api_key_valid( $value, 'google' ) ) { - return 'invalid_key'; - } return _gutenberg_mask_api_key( $value ); } @@ -131,20 +128,17 @@ function _gutenberg_validate_gemini_api_key_on_save( $value, $old_value ) { // --- OpenAI --- /** - * Masks and validates the OpenAI API key on read. + * Masks the OpenAI API key on read. * * @access private * * @param string $value The raw option value. - * @return string Masked key, 'invalid_key', or empty string. + * @return string Masked key or empty string. */ function _gutenberg_mask_openai_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === _gutenberg_is_api_key_valid( $value, 'openai' ) ) { - return 'invalid_key'; - } return _gutenberg_mask_api_key( $value ); } @@ -168,20 +162,17 @@ function _gutenberg_validate_openai_api_key_on_save( $value, $old_value ) { // --- Anthropic --- /** - * Masks and validates the Anthropic API key on read. + * Masks the Anthropic API key on read. * * @access private * * @param string $value The raw option value. - * @return string Masked key, 'invalid_key', or empty string. + * @return string Masked key or empty string. */ function _gutenberg_mask_anthropic_api_key( $value ) { if ( empty( $value ) ) { return $value; } - if ( false === _gutenberg_is_api_key_valid( $value, 'anthropic' ) ) { - return 'invalid_key'; - } return _gutenberg_mask_api_key( $value ); } @@ -205,43 +196,55 @@ function _gutenberg_validate_anthropic_api_key_on_save( $value, $old_value ) { // --- REST API filtering --- /** - * Short-circuits get_option for connector API key settings that were not - * explicitly requested via _fields on the REST settings endpoint. + * Validates connector API keys in the REST response when explicitly requested. * - * This prevents the mask filter (and its expensive isProviderConfigured - * validation) from running for connector settings that the caller did - * not ask for. + * Runs on `rest_post_dispatch` for `/wp/v2/settings` requests that include + * connector fields via `_fields`. For each requested connector field, reads + * the real (unmasked) key, validates it against the provider, and replaces + * the response value with 'invalid_key' if validation fails. * * @access private * - * @param mixed $response Result to send to the client. Usually null. - * @param WP_REST_Server $server The server instance. - * @param WP_REST_Request $request The request object. - * @return mixed Unmodified $response. + * @param WP_REST_Response $response The response object. + * @param WP_REST_Server $server The server instance. + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The potentially modified response. */ -function _gutenberg_skip_unrequested_connector_validation( $response, $server, $request ) { +function _gutenberg_validate_connector_keys_in_rest( $response, $server, $request ) { if ( '/wp/v2/settings' !== $request->get_route() ) { return $response; } - $fields = $request->get_param( '_fields' ); - $requested = $fields ? array_map( 'trim', explode( ',', $fields ) ) : array(); + $fields = $request->get_param( '_fields' ); + if ( ! $fields ) { + return $response; + } - $connector_options = array( - 'connectors_gemini_api_key', - 'connectors_openai_api_key', - 'connectors_anthropic_api_key', + $requested = array_map( 'trim', explode( ',', $fields ) ); + $data = $response->get_data(); + $connectors = array( + 'connectors_gemini_api_key' => array( 'google', '_gutenberg_mask_gemini_api_key' ), + 'connectors_openai_api_key' => array( 'openai', '_gutenberg_mask_openai_api_key' ), + 'connectors_anthropic_api_key' => array( 'anthropic', '_gutenberg_mask_anthropic_api_key' ), ); - foreach ( $connector_options as $option_name ) { + foreach ( $connectors as $option_name => $config ) { if ( ! in_array( $option_name, $requested, true ) ) { - add_filter( "pre_option_{$option_name}", '__return_empty_string' ); + continue; + } + $real_key = _gutenberg_get_real_api_key( $option_name, $config[1] ); + if ( empty( $real_key ) ) { + continue; + } + if ( false === _gutenberg_is_api_key_valid( $real_key, $config[0] ) ) { + $data[ $option_name ] = 'invalid_key'; } } + $response->set_data( $data ); return $response; } -add_filter( 'rest_pre_dispatch', '_gutenberg_skip_unrequested_connector_validation', 10, 3 ); +add_filter( 'rest_post_dispatch', '_gutenberg_validate_connector_keys_in_rest', 10, 3 ); // --- Registration --- From 9985d042ecd6f8141f608bd3fcac8e18056ce20f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:28:56 +0000 Subject: [PATCH 49/81] avoid loading example extender --- lib/experimental/connectors/load.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index 4b7154cf748da1..8c6004011b143b 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -36,7 +36,7 @@ function gutenberg_register_connectors_extension_module() { filemtime( __DIR__ . '/connectors-extension.js' ) ); } -add_action( 'init', 'gutenberg_register_connectors_extension_module' ); +//add_action( 'init', 'gutenberg_register_connectors_extension_module' ); /** * Enqueues the connectors extension on the Connectors page. From 023afbd444b7c02d99d98ff50d446c113e410207 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:47:25 +0000 Subject: [PATCH 50/81] UI improvements --- packages/connectors/src/connector-item.tsx | 4 ++-- routes/connectors-home/default-connectors.tsx | 18 +++++++++--------- routes/connectors-home/stage.tsx | 2 +- routes/connectors-home/style.scss | 8 +------- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 2975857720223d..7d525e56ba0b53 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -43,8 +43,8 @@ export function ConnectorItem( { { icon } - { name } - { description } + { name } + { description } { actionArea } diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 1a909b05aa886d..70468e047f12e7 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -18,10 +18,10 @@ import { useConnectorPlugin } from './use-connector-plugin'; const ConnectedBadge = () => ( ( // OpenAI logo as inline SVG const OpenAILogo = () => ( ( // Claude/Anthropic logo as inline SVG const ClaudeLogo = () => ( ( // Gemini logo as inline SVG const GeminiLogo = () => (
- + { connectors.map( ( connector: ConnectorConfig ) => { if ( connector.render ) { return ( diff --git a/routes/connectors-home/style.scss b/routes/connectors-home/style.scss index 01d92ccfc1e51d..bcc8bf64af36d9 100644 --- a/routes/connectors-home/style.scss +++ b/routes/connectors-home/style.scss @@ -4,14 +4,8 @@ margin: 0 auto; padding: 24px; - .components-item-group { - gap: 12px; - display: flex; - flex-direction: column; - } - .components-item { - padding: 16px 20px; + padding: 20px; border-radius: 8px; border: 1px solid #ddd; background: #fff; From dd111ba4aca4459b7ed30cb1650fb85b958a8bad Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:50:04 +0000 Subject: [PATCH 51/81] make functions private --- lib/experimental/connectors/load.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index 8c6004011b143b..0cb72a7a2ea473 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -5,12 +5,14 @@ * @package gutenberg */ -add_action( 'admin_menu', 'gutenberg_connectors_add_settings_menu_item' ); +add_action( 'admin_menu', '_gutenberg_connectors_add_settings_menu_item' ); /** * Registers the Connectors menu item under Settings. + * + * @access private */ -function gutenberg_connectors_add_settings_menu_item() { +function _gutenberg_connectors_add_settings_menu_item() { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } @@ -27,8 +29,10 @@ function gutenberg_connectors_add_settings_menu_item() { /** * Registers the example connectors extension as a script module. + * + * @access private */ -function gutenberg_register_connectors_extension_module() { +function _gutenberg_register_connectors_extension_module() { wp_register_script_module( 'gutenberg/connectors-extension', gutenberg_url( 'lib/experimental/connectors/connectors-extension.js' ), @@ -36,20 +40,22 @@ function gutenberg_register_connectors_extension_module() { filemtime( __DIR__ . '/connectors-extension.js' ) ); } -//add_action( 'init', 'gutenberg_register_connectors_extension_module' ); +//add_action( 'init', '_gutenberg_register_connectors_extension_module' ); /** * Enqueues the connectors extension on the Connectors page. * + * @access private + * * @param string $hook_suffix The current admin page. */ -function gutenberg_enqueue_connectors_extension( $hook_suffix ) { +function _gutenberg_enqueue_connectors_extension( $hook_suffix ) { if ( 'settings_page_connectors-wp-admin' !== $hook_suffix ) { return; } wp_enqueue_script_module( 'gutenberg/connectors-extension' ); } -add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_connectors_extension' ); +add_action( 'admin_enqueue_scripts', '_gutenberg_enqueue_connectors_extension' ); require __DIR__ . '/default-connectors.php'; require __DIR__ . '/debug-test.php'; From 82890c76727cccd369619a39387c0dfbd410737b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:53:35 +0000 Subject: [PATCH 52/81] fix dimensions --- routes/connectors-home/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/connectors-home/style.scss b/routes/connectors-home/style.scss index bcc8bf64af36d9..7247ec536edfb5 100644 --- a/routes/connectors-home/style.scss +++ b/routes/connectors-home/style.scss @@ -1,6 +1,6 @@ .connectors-page { width: 100%; - max-width: 800px; + max-width: 680px; margin: 0 auto; padding: 24px; From 19757e7439c30e7fe4126b90d9b40f0dc6b05e28 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 12:58:25 +0000 Subject: [PATCH 53/81] Extract logos to a separate file --- routes/connectors-home/default-connectors.tsx | 99 +------------------ routes/connectors-home/logos.tsx | 97 ++++++++++++++++++ 2 files changed, 98 insertions(+), 98 deletions(-) create mode 100644 routes/connectors-home/logos.tsx diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 70468e047f12e7..30c5733040d6de 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -14,6 +14,7 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import { useConnectorPlugin } from './use-connector-plugin'; +import { OpenAILogo, ClaudeLogo, GeminiLogo } from './logos'; const ConnectedBadge = () => ( ( ); -// OpenAI logo as inline SVG -const OpenAILogo = () => ( - - - -); - -// Claude/Anthropic logo as inline SVG -const ClaudeLogo = () => ( - - - -); - -// Gemini logo as inline SVG -const GeminiLogo = () => ( - - - - - - - - - - - - - - - - - - - - -); - interface ConnectorConfig { pluginSlug: string; settingName: string; diff --git a/routes/connectors-home/logos.tsx b/routes/connectors-home/logos.tsx new file mode 100644 index 00000000000000..bdba2db470aa26 --- /dev/null +++ b/routes/connectors-home/logos.tsx @@ -0,0 +1,97 @@ +// OpenAI logo as inline SVG +export const OpenAILogo = () => ( + + + +); + +// Claude/Anthropic logo as inline SVG +export const ClaudeLogo = () => ( + + + +); + +// Gemini logo as inline SVG +export const GeminiLogo = () => ( + + + + + + + + + + + + + + + + + + + + +); From 8cdc3c517fa77f09cecf48be8fde4449c757a8f9 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 14:36:19 +0000 Subject: [PATCH 54/81] fix key already exists after dispatch --- routes/connectors-home/use-connector-plugin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts index 9ab1da78654931..4ce26d0370b1c8 100644 --- a/routes/connectors-home/use-connector-plugin.ts +++ b/routes/connectors-home/use-connector-plugin.ts @@ -92,6 +92,7 @@ export function useConnectorPlugin( { data: { slug: pluginSlug, status: 'active' }, } ); setPluginStatus( 'active' ); + await fetchApiKey(); setIsExpanded( true ); } catch { // Handle error @@ -109,6 +110,7 @@ export function useConnectorPlugin( { data: { status: 'active' }, } ); setPluginStatus( 'active' ); + await fetchApiKey(); setIsExpanded( true ); } catch { // Handle error From 579e63ace3c78b2d173d2c10c657a7aefe1012d0 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 14:53:25 +0000 Subject: [PATCH 55/81] remove connector example code --- .../connectors/connectors-extension.js | 51 ------------------- lib/experimental/connectors/load.php | 30 ----------- 2 files changed, 81 deletions(-) delete mode 100644 lib/experimental/connectors/connectors-extension.js diff --git a/lib/experimental/connectors/connectors-extension.js b/lib/experimental/connectors/connectors-extension.js deleted file mode 100644 index 49b0d71e687bea..00000000000000 --- a/lib/experimental/connectors/connectors-extension.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Example extension script for the Connectors page. - * Demonstrates how plugins can register a connector using the @wordpress/connectors API. - * - * This script registers a "Hello World" connector to show how plugins can - * add their own connectors to the Connectors settings page. - * - * Note: @wordpress/connectors is imported as a script module, while - * wp.element and wp.components are accessed as globals (no build step needed). - */ - -/* global wp */ -import { registerConnector, ConnectorItem } from '@wordpress/connectors'; - -const { useState, createElement } = wp.element; -const { Button } = wp.components; - -// Hello World connector render component -function HelloWorldConnector( { label, description } ) { - const [ isExpanded, setIsExpanded ] = useState( false ); - - return createElement( - ConnectorItem, - { - name: label, - description, - actionArea: createElement( - Button, - { - variant: 'secondary', - size: 'compact', - onClick: () => setIsExpanded( ! isExpanded ), - 'aria-expanded': isExpanded, - }, - isExpanded ? 'Close' : 'Configure' - ), - }, - isExpanded && - createElement( 'p', null, 'Hello World settings would go here!' ) - ); -} - -// Register the Hello World connector -registerConnector( 'example/hello-world', { - label: 'Hello World', - description: 'A simple example connector registered via script module.', - render: HelloWorldConnector, -} ); - -// eslint-disable-next-line no-console -console.log( 'Hello World connector registered!' ); diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index 0cb72a7a2ea473..b7ad20b30b6d4e 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -27,35 +27,5 @@ function _gutenberg_connectors_add_settings_menu_item() { ); } -/** - * Registers the example connectors extension as a script module. - * - * @access private - */ -function _gutenberg_register_connectors_extension_module() { - wp_register_script_module( - 'gutenberg/connectors-extension', - gutenberg_url( 'lib/experimental/connectors/connectors-extension.js' ), - array( '@wordpress/connectors' ), - filemtime( __DIR__ . '/connectors-extension.js' ) - ); -} -//add_action( 'init', '_gutenberg_register_connectors_extension_module' ); - -/** - * Enqueues the connectors extension on the Connectors page. - * - * @access private - * - * @param string $hook_suffix The current admin page. - */ -function _gutenberg_enqueue_connectors_extension( $hook_suffix ) { - if ( 'settings_page_connectors-wp-admin' !== $hook_suffix ) { - return; - } - wp_enqueue_script_module( 'gutenberg/connectors-extension' ); -} -add_action( 'admin_enqueue_scripts', '_gutenberg_enqueue_connectors_extension' ); - require __DIR__ . '/default-connectors.php'; require __DIR__ . '/debug-test.php'; From b72cea2b6a82b370cad2469e4dcd5b2854452b11 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 14:56:10 +0000 Subject: [PATCH 56/81] add loading check --- lib/experimental/connectors/load.php | 3 --- lib/load.php | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/experimental/connectors/load.php b/lib/experimental/connectors/load.php index b7ad20b30b6d4e..676b40880d87ec 100644 --- a/lib/experimental/connectors/load.php +++ b/lib/experimental/connectors/load.php @@ -13,9 +13,6 @@ * @access private */ function _gutenberg_connectors_add_settings_menu_item() { - if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { - return; - } add_submenu_page( 'options-general.php', __( 'Connectors', 'gutenberg' ), diff --git a/lib/load.php b/lib/load.php index 84dedb70656619..c5b4a28254ffdc 100644 --- a/lib/load.php +++ b/lib/load.php @@ -122,7 +122,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/pages/site-editor.php'; require __DIR__ . '/experimental/extensible-site-editor.php'; require __DIR__ . '/experimental/fonts/load.php'; -require __DIR__ . '/experimental/connectors/load.php'; +if ( class_exists( '\WordPress\AiClient\AiClient' ) ) { + require __DIR__ . '/experimental/connectors/load.php'; +} if ( gutenberg_is_experiment_enabled( 'gutenberg-workflow-palette' ) ) { require __DIR__ . '/experimental/workflow-palette.php'; From 43ce449c1d0bad2581fd2d973599efbbdf4993b4 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 16:48:46 +0000 Subject: [PATCH 57/81] make api's private and experimental --- packages/connectors/src/api.ts | 14 +++++--------- packages/connectors/src/index.ts | 9 ++++++--- packages/connectors/src/lock-unlock.ts | 10 ++++++++++ packages/connectors/src/private-apis.ts | 8 ++++++++ packages/connectors/src/store.ts | 5 +++-- packages/private-apis/src/implementation.ts | 1 + routes/connectors-home/default-connectors.tsx | 6 +++--- routes/connectors-home/stage.tsx | 7 +++++-- 8 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 packages/connectors/src/lock-unlock.ts create mode 100644 packages/connectors/src/private-apis.ts diff --git a/packages/connectors/src/api.ts b/packages/connectors/src/api.ts index dac1928882c132..f427e720ae54a4 100644 --- a/packages/connectors/src/api.ts +++ b/packages/connectors/src/api.ts @@ -6,11 +6,9 @@ import { dispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { store, STORE_NAME } from './store'; -import { ConnectorItem, DefaultConnectorSettings } from './connector-item'; -import type { ConnectorConfig, ConnectorRenderProps } from './types'; - -export type { ConnectorConfig, ConnectorRenderProps }; +import { store } from './store'; +import { unlock } from './lock-unlock'; +import type { ConnectorConfig } from './types'; /** * Register a connector that will appear in the Connectors settings page. @@ -20,7 +18,7 @@ export type { ConnectorConfig, ConnectorRenderProps }; * * @example * ```js - * import { registerConnector, ConnectorItem } from '@wordpress/connectors'; + * import { __experimentalRegisterConnector as registerConnector, __experimentalConnectorItem as ConnectorItem } from '@wordpress/connectors'; * * registerConnector( 'my-plugin/openai', { * label: 'OpenAI', @@ -42,7 +40,5 @@ export function registerConnector( slug: string, config: Omit< ConnectorConfig, 'slug' > ): void { - dispatch( store ).registerConnector( slug, config ); + unlock( dispatch( store ) ).registerConnector( slug, config ); } - -export { ConnectorItem, DefaultConnectorSettings, store, STORE_NAME }; diff --git a/packages/connectors/src/index.ts b/packages/connectors/src/index.ts index fef8b30dfaa1fe..bcf5025d6dc7c6 100644 --- a/packages/connectors/src/index.ts +++ b/packages/connectors/src/index.ts @@ -1,4 +1,7 @@ -export { registerConnector } from './api'; -export { ConnectorItem, DefaultConnectorSettings } from './connector-item'; -export { store, STORE_NAME } from './store'; +export { registerConnector as __experimentalRegisterConnector } from './api'; +export { + ConnectorItem as __experimentalConnectorItem, + DefaultConnectorSettings as __experimentalDefaultConnectorSettings, +} from './connector-item'; export type { ConnectorConfig, ConnectorRenderProps } from './types'; +export { privateApis } from './private-apis'; diff --git a/packages/connectors/src/lock-unlock.ts b/packages/connectors/src/lock-unlock.ts new file mode 100644 index 00000000000000..3b6e8ab1bb0067 --- /dev/null +++ b/packages/connectors/src/lock-unlock.ts @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/connectors' + ); diff --git a/packages/connectors/src/private-apis.ts b/packages/connectors/src/private-apis.ts new file mode 100644 index 00000000000000..963df3a09b48ad --- /dev/null +++ b/packages/connectors/src/private-apis.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { lock } from './lock-unlock'; +import { store, STORE_NAME } from './store'; + +export const privateApis: Record< string, never > = {}; +lock( privateApis, { store, STORE_NAME } ); diff --git a/packages/connectors/src/store.ts b/packages/connectors/src/store.ts index 57ec3a815c417e..1335e050d845fc 100644 --- a/packages/connectors/src/store.ts +++ b/packages/connectors/src/store.ts @@ -7,6 +7,7 @@ import { createReduxStore, register } from '@wordpress/data'; * Internal dependencies */ import type { ConnectorConfig, ConnectorsState } from './types'; +import { unlock } from './lock-unlock'; const STORE_NAME = 'core/connectors'; @@ -61,10 +62,10 @@ const selectors = { export const store = createReduxStore( STORE_NAME, { reducer, - actions, - selectors, } ); register( store ); +unlock( store ).registerPrivateActions( actions ); +unlock( store ).registerPrivateSelectors( selectors ); export { STORE_NAME }; diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index e6794d89c806af..6f50a880556253 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -16,6 +16,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/blocks', '@wordpress/boot', '@wordpress/commands', + '@wordpress/connectors', '@wordpress/workflows', '@wordpress/components', '@wordpress/core-commands', diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 30c5733040d6de..1519860ed859d0 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -3,9 +3,9 @@ */ import { __experimentalHStack as HStack, Button } from '@wordpress/components'; import { - registerConnector, - ConnectorItem, - DefaultConnectorSettings, + __experimentalRegisterConnector as registerConnector, + __experimentalConnectorItem as ConnectorItem, + __experimentalDefaultConnectorSettings as DefaultConnectorSettings, type ConnectorRenderProps, } from '@wordpress/connectors'; import { __, sprintf } from '@wordpress/i18n'; diff --git a/routes/connectors-home/stage.tsx b/routes/connectors-home/stage.tsx index 81929cfd137654..1e7259b9cad593 100644 --- a/routes/connectors-home/stage.tsx +++ b/routes/connectors-home/stage.tsx @@ -3,7 +3,7 @@ */ import { Page } from '@wordpress/admin-ui'; import { __experimentalVStack as VStack } from '@wordpress/components'; -import { store, type ConnectorConfig } from '@wordpress/connectors'; +import { type ConnectorConfig, privateApis as connectorsPrivateApis } from '@wordpress/connectors'; import { useSelect } from '@wordpress/data'; import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -13,13 +13,16 @@ import { __ } from '@wordpress/i18n'; */ import './style.scss'; import { registerDefaultConnectors } from './default-connectors'; +import { unlock } from '../lock-unlock'; + +const { store } = unlock( connectorsPrivateApis ); // Register built-in connectors registerDefaultConnectors(); function ConnectorsPage() { const connectors = useSelect( - ( select ) => select( store ).getConnectors(), + ( select ) => unlock( select( store ) ).getConnectors(), [] ); From 93ae861672026ac9e02deca5f4fb402874cf9c31 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 25 Feb 2026 17:30:39 +0000 Subject: [PATCH 58/81] remove client validaiton --- packages/connectors/src/connector-item.tsx | 18 +---- routes/connectors-home/default-connectors.tsx | 78 +------------------ 2 files changed, 3 insertions(+), 93 deletions(-) diff --git a/packages/connectors/src/connector-item.tsx b/packages/connectors/src/connector-item.tsx index 7d525e56ba0b53..7256f681ef6d39 100644 --- a/packages/connectors/src/connector-item.tsx +++ b/packages/connectors/src/connector-item.tsx @@ -62,7 +62,6 @@ export interface DefaultConnectorSettingsProps { helpUrl?: string; helpLabel?: string; readOnly?: boolean; - validate?: ( value: string ) => string | undefined; } /** @@ -75,16 +74,11 @@ export function DefaultConnectorSettings( { helpUrl, helpLabel, readOnly = false, - validate, }: DefaultConnectorSettingsProps ) { const [ apiKey, setApiKey ] = useState( initialValue ); - const [ hasBlurred, setHasBlurred ] = useState( false ); const [ isSaving, setIsSaving ] = useState( false ); const [ saveError, setSaveError ] = useState< string | null >( null ); - const validationError = - ! readOnly && hasBlurred && apiKey ? validate?.( apiKey ) : undefined; - const helpLinkLabel = helpLabel || helpUrl?.replace( /^https?:\/\//, '' ); @@ -115,13 +109,6 @@ export function DefaultConnectorSettings( { ); } - if ( validationError ) { - return ( - - { validationError } - - ); - } return helpLink; }; @@ -134,7 +121,7 @@ export function DefaultConnectorSettings( { setSaveError( error instanceof Error ? error.message - : __( 'Failed to save API key. Please try again.' ) + : __( 'It was not possible to connect to the provider using this key.' ) ); } finally { setIsSaving( false ); @@ -155,7 +142,6 @@ export function DefaultConnectorSettings( { setSaveError( null ); setApiKey( value ); } } - onBlur={ () => setHasBlurred( true ) } placeholder="YOUR_API_KEY" disabled={ readOnly || isSaving } help={ getHelp() } @@ -172,7 +158,7 @@ export function DefaultConnectorSettings( { ) : (