diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php index 0480bb766c323c..fe3c805fc32b37 100644 --- a/lib/experimental/connectors/default-connectors.php +++ b/lib/experimental/connectors/default-connectors.php @@ -427,8 +427,6 @@ function _gutenberg_get_connector_script_module_data( array $data ): array { return $data; } - $registry = \WordPress\AiClient\AiClient::defaultRegistry(); - // Build a slug-to-file map for plugin installation status. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -448,11 +446,6 @@ function _gutenberg_get_connector_script_module_data( array $data ): array { $auth_out['settingName'] = $auth['setting_name'] ?? ''; $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; $auth_out['keySource'] = _gutenberg_get_api_key_source( $connector_id, $auth['setting_name'] ?? '' ); - try { - $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); - } catch ( Exception $e ) { - $auth_out['isConnected'] = false; - } } $connector_out = array( @@ -485,3 +478,59 @@ function _gutenberg_get_connector_script_module_data( array $data ): array { } remove_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' ); add_filter( 'script_module_data_options-connectors-wp-admin', '_gutenberg_get_connector_script_module_data' ); + +/** + * Streams connector connection-status checks to the browser after the page + * has been flushed, so the initial render is not blocked by slow HTTP + * validation requests. + * + * Each resolved status is delivered as an inline ', + wp_json_encode( $connector_id ), + $is_connected ? 'true' : 'false', + wp_json_encode( $connector_id ), + $is_connected ? 'true' : 'false' + ); + + // Flush each result so the browser processes it immediately. + flush(); + } +} +add_action( 'admin_footer-settings_page_options-connectors-wp-admin', '_gutenberg_stream_connector_statuses' ); diff --git a/routes/connectors-home/ai-plugin-callout.tsx b/routes/connectors-home/ai-plugin-callout.tsx index e008cd5ec4ed1f..62e0c8326e60d2 100644 --- a/routes/connectors-home/ai-plugin-callout.tsx +++ b/routes/connectors-home/ai-plugin-callout.tsx @@ -34,13 +34,13 @@ export function AiPluginCallout() { const [ isBusy, setIsBusy ] = useState( false ); const [ justActivated, setJustActivated ] = useState( false ); - // Server-side initial state — true if any provider was already connected at page load. + // Server-side initial state — true if any provider has a configured key at page load. const initialHasConnectedProvider = useRef( connectorDataValues.some( ( c ) => c.type === 'ai_provider' && c.authentication.method === 'api_key' && - c.authentication.isConnected + c.authentication.keySource !== 'none' ) ).current; diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index d12be5228ca6f7..8668576706f7fd 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -29,7 +29,6 @@ type ConnectorAuthentication = settingName: string; credentialsUrl: string | null; keySource?: ApiKeySource; - isConnected?: boolean; } | { method: 'none' }; @@ -102,6 +101,7 @@ const ConnectedBadge = () => ( const UnavailableActionBadge = () => { __( 'Not available' ) }; interface ApiKeyConnectorConfig { + connectorId: string; pluginSlug?: string; settingName: string; helpUrl?: string; @@ -109,12 +109,12 @@ interface ApiKeyConnectorConfig { isInstalled?: boolean; isActivated?: boolean; keySource?: ApiKeySource; - initialIsConnected?: boolean; } function ApiKeyConnector( { label, description, + connectorId, pluginSlug, settingName, helpUrl, @@ -122,7 +122,6 @@ function ApiKeyConnector( { isInstalled, isActivated, keySource: initialKeySource, - initialIsConnected, }: ConnectorRenderProps & ApiKeyConnectorConfig ) { let helpLabel: string | undefined; try { @@ -141,6 +140,7 @@ function ApiKeyConnector( { setIsExpanded, isBusy, isConnected, + isCheckingConnection, currentApiKey, keySource, handleButtonClick, @@ -148,12 +148,12 @@ function ApiKeyConnector( { saveApiKey, removeApiKey, } = useConnectorPlugin( { + connectorId, pluginSlug, settingName, isInstalled, isActivated, keySource: initialKeySource, - initialIsConnected, } ); const isExternallyConfigured = keySource === 'env' || keySource === 'constant'; @@ -187,7 +187,11 @@ function ApiKeyConnector( { : 'compact' } onClick={ handleButtonClick } - disabled={ pluginStatus === 'checking' || isBusy } + disabled={ + pluginStatus === 'checking' || + isCheckingConnection || + isBusy + } isBusy={ isBusy } aria-expanded={ isExpanded } > @@ -247,6 +251,7 @@ export function registerDefaultConnectors() { render: ( props ) => ( ), } ); diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts index 9287f4abc5234c..34d2d4f51ad708 100644 --- a/routes/connectors-home/use-connector-plugin.ts +++ b/routes/connectors-home/use-connector-plugin.ts @@ -3,7 +3,7 @@ */ import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/connectors'; @@ -11,12 +11,12 @@ import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/conn export type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active'; interface UseConnectorPluginOptions { + connectorId: string; pluginSlug?: string; settingName: string; isInstalled?: boolean; isActivated?: boolean; keySource?: ApiKeySource; - initialIsConnected?: boolean; } interface UseConnectorPluginReturn { @@ -27,6 +27,7 @@ interface UseConnectorPluginReturn { setIsExpanded: ( expanded: boolean ) => void; isBusy: boolean; isConnected: boolean; + isCheckingConnection: boolean; currentApiKey: string; keySource: ApiKeySource; handleButtonClick: () => void; @@ -36,17 +37,53 @@ interface UseConnectorPluginReturn { } export function useConnectorPlugin( { + connectorId, pluginSlug, settingName, isInstalled, isActivated, keySource = 'none', - initialIsConnected = false, }: UseConnectorPluginOptions ): UseConnectorPluginReturn { const [ isExpanded, setIsExpanded ] = useState( false ); const [ isBusy, setIsBusy ] = useState( false ); - const [ connectedState, setConnectedState ] = - useState( initialIsConnected ); + const [ connectedState, setConnectedState ] = useState< boolean | null >( + () => { + // Check if streaming already delivered the result before React mounted. + const streamed = ( + window as unknown as { + __connectorStatuses?: Record< string, boolean >; + } + ).__connectorStatuses?.[ connectorId ]; + return streamed !== undefined ? streamed : null; + } + ); + + // Listen for streamed connector-status events from the server. + useEffect( () => { + if ( connectedState !== null ) { + return; + } + const handler = ( e: Event ) => { + const detail = ( e as CustomEvent ).detail; + if ( detail.id === connectorId ) { + setConnectedState( detail.connected ); + } + }; + document.addEventListener( 'connector-status', handler ); + + // Check again in case the event fired between initial state and effect registration. + const streamed = ( + window as unknown as { + __connectorStatuses?: Record< string, boolean >; + } + ).__connectorStatuses?.[ connectorId ]; + if ( streamed !== undefined ) { + setConnectedState( streamed ); + } + + return () => + document.removeEventListener( 'connector-status', handler ); + }, [ connectorId, connectedState ] ); // Local override for immediate UI feedback after install/activate. const [ pluginStatusOverride, setPluginStatusOverride ] = useState< PluginStatus | null >( null ); @@ -144,8 +181,11 @@ export function useConnectorPlugin( { // Use canManagePlugins (from plugin entity resolution) for activation capability. const canActivatePlugins = canManagePlugins; + const isCheckingConnection = + pluginStatus === 'active' && connectedState === null; + const isConnected = - ( pluginStatus === 'active' && connectedState ) || + ( pluginStatus === 'active' && connectedState === true ) || // After install/activate, if settings re-fetch reveals an existing key, // update connected state (mirrors what the server would report on page load). ( pluginStatusOverride === 'active' && !! currentApiKey ); @@ -226,6 +266,9 @@ export function useConnectorPlugin( { if ( isConnected ) { return __( 'Edit' ); } + if ( isCheckingConnection ) { + return __( 'Checking…' ); + } switch ( pluginStatus ) { case 'checking': return __( 'Checking…' ); @@ -298,6 +341,7 @@ export function useConnectorPlugin( { setIsExpanded, isBusy, isConnected, + isCheckingConnection, currentApiKey, keySource, handleButtonClick,