Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3753ec9
feat: add file mod status check and display notice for disabled insta…
coderGtm Apr 21, 2026
6591709
feat: enhance plugin callout button visibility based on installation …
coderGtm Apr 22, 2026
e6ee93b
feat: update connector search link based on file mod status and alway…
coderGtm Apr 23, 2026
74813ab
feat: update notice status to "notice" from "warning"
coderGtm Apr 23, 2026
2dcd6a6
feat: update connector search link to use translation function for be…
coderGtm Apr 24, 2026
fb1dddc
feat: simplify plugin installation notice for better clarity
coderGtm Apr 24, 2026
bd8a867
feat: enhance connectors page to include AI plugin activation status …
coderGtm Apr 27, 2026
3340a5c
feat: enhance WP-CLI examples section with collapsible details for im…
coderGtm Apr 28, 2026
b3b05ed
feat: enhance manual installation notice to include permission check
coderGtm Apr 28, 2026
3fbd434
feat: refactor notice component to use newer component
coderGtm May 7, 2026
3b85c02
Avoid string concatenation
t-hamano May 8, 2026
9fe9e43
Remove unnecessary top margin
t-hamano May 8, 2026
b955d23
Apply suggestions from code review
jorgefilipecosta May 8, 2026
1f3091e
feat: drop WP-CLI examples and link to plugin directory for unavailab…
jorgefilipecosta May 8, 2026
24580fa
fix: notice should track plugin install status, not activation
jorgefilipecosta May 8, 2026
ae57aaf
Update routes/connectors-home/default-connectors.tsx
jorgefilipecosta May 8, 2026
65b4012
refactor: use Stack from @wordpress/ui instead of __experimentalHStack
jorgefilipecosta May 8, 2026
40e1a3e
test: update connectors capability tests for Learn more link
jorgefilipecosta May 8, 2026
64041e0
Revert HStack to Stack change to minimize PR diff
jorgefilipecosta May 8, 2026
7027f7b
Remove wordpress.org search fallback; hide link when restricted
jorgefilipecosta May 8, 2026
3e82fd2
Rename isFileModsDisabled to isFileModDisabled
jorgefilipecosta May 8, 2026
23f2ee5
Fix variable assignment alignment PHPCS issue
westonruter May 8, 2026
df811be
Add backport changelog for core PR 11779
jorgefilipecosta May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backport-changelog/7.0/11779.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/11779

* https://github.com/WordPress/gutenberg/pull/77521
3 changes: 2 additions & 1 deletion lib/experimental/connectors/default-connectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,8 @@ function _gutenberg_get_connector_script_module_data( array $data ): array {
$connectors[ $connector_id ] = $connector_out;
}
ksort( $connectors );
$data['connectors'] = $connectors;
$data['connectors'] = $connectors;
$data['isFileModDisabled'] = ! wp_is_file_mod_allowed( 'install_plugins' );
return $data;
}
remove_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' );
Expand Down
54 changes: 26 additions & 28 deletions routes/connectors-home/ai-plugin-callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,6 @@ export function AiPluginCallout() {
return null;
}

// Not installed and no permissions to install.
if ( pluginStatus === 'not-installed' && canInstallPlugins === false ) {
return null;
}

// Installed but can't activate (no manage permissions).
if ( pluginStatus === 'inactive' && canManagePlugins === false ) {
return null;
Expand All @@ -215,6 +210,8 @@ export function AiPluginCallout() {
( ! initialHasConnectedProvider || justActivated );
const showInstallActivate =
pluginStatus === 'not-installed' || pluginStatus === 'inactive';
const hideButtons =
pluginStatus === 'not-installed' && canInstallPlugins === false;

const getMessage = () => {
if ( isJustConnected ) {
Expand Down Expand Up @@ -262,29 +259,30 @@ export function AiPluginCallout() {
a: <ExternalLink href={ AI_PLUGIN_URL } />,
} ) }
</p>
{ showInstallActivate ? (
<Button
variant="primary"
size="compact"
isBusy={ isBusy }
disabled={ getPrimaryButtonProps().disabled }
accessibleWhenDisabled
onClick={ getPrimaryButtonProps().onClick }
>
{ getPrimaryButtonProps().label }
</Button>
) : (
<Button
ref={ actionButtonRef }
variant="secondary"
size="compact"
href={ addQueryArgs( 'options-general.php', {
page: AI_PLUGIN_PAGE_SLUG,
} ) }
>
{ __( 'Control features in the AI plugin' ) }
</Button>
) }
{ ! hideButtons &&
( showInstallActivate ? (
<Button
variant="primary"
size="compact"
isBusy={ isBusy }
disabled={ getPrimaryButtonProps().disabled }
accessibleWhenDisabled
onClick={ getPrimaryButtonProps().onClick }
>
{ getPrimaryButtonProps().label }
</Button>
) : (
<Button
ref={ actionButtonRef }
variant="secondary"
size="compact"
href={ addQueryArgs( 'options-general.php', {
page: AI_PLUGIN_PAGE_SLUG,
} ) }
>
{ __( 'Control features in the AI plugin' ) }
</Button>
) ) }
</div>
<WpLogoDecoration />
</div>
Expand Down
50 changes: 40 additions & 10 deletions routes/connectors-home/default-connectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
type ConnectorRenderProps,
} from '@wordpress/connectors';
import { select } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Badge } from '@wordpress/ui';
import { __, sprintf } from '@wordpress/i18n';
import { Badge, Link } from '@wordpress/ui';

/**
* Internal dependencies
Expand Down Expand Up @@ -43,22 +43,34 @@ interface ConnectorData {
authentication: NonNullable< ConnectorConfig[ 'authentication' ] >;
}

/**
* Reads connector data passed from PHP via the script module data mechanism.
*/
export function getConnectorData(): Record< string, ConnectorData > {
interface ConnectorScriptModuleData {
connectors?: Record< string, ConnectorData >;
isFileModDisabled?: boolean;
}

function getConnectorScriptModuleData(): ConnectorScriptModuleData {
try {
const parsed = JSON.parse(
return JSON.parse(
document.getElementById(
'wp-script-module-data-options-connectors-wp-admin'
)?.textContent ?? ''
)?.textContent ?? '{}'
);
return parsed?.connectors ?? {};
} catch {
return {};
}
}

/**
* Reads connector data passed from PHP via the script module data mechanism.
*/
export function getConnectorData(): Record< string, ConnectorData > {
return getConnectorScriptModuleData().connectors ?? {};
}

export function getIsFileModDisabled(): boolean {
return !! getConnectorScriptModuleData().isFileModDisabled;
}

const CONNECTOR_LOGOS: Record< string, React.ComponentType > = {
google: GeminiLogo,
openai: OpenAILogo,
Expand Down Expand Up @@ -96,6 +108,19 @@ const ConnectedBadge = () => (
</span>
);

const PluginDirectoryLink = ( { slug }: { slug: string } ) => (
<Link
href={ sprintf(
/* translators: %s: plugin slug. */
__( 'https://wordpress.org/plugins/%s/' ),
slug
) }
openInNewTab
>
{ __( 'Learn more' ) }
</Link>
);

const UnavailableActionBadge = () => <Badge>{ __( 'Not available' ) }</Badge>;

function ApiKeyConnector( {
Expand Down Expand Up @@ -166,7 +191,12 @@ function ApiKeyConnector( {
actionArea={
<HStack spacing={ 3 } expanded={ false }>
{ isConnected && <ConnectedBadge /> }
{ showUnavailableBadge && <UnavailableActionBadge /> }
{ showUnavailableBadge &&
( pluginSlug ? (
<PluginDirectoryLink slug={ pluginSlug } />
) : (
<UnavailableActionBadge />
) ) }
{ showActionButton && (
<Button
ref={ actionButtonRef }
Expand Down
84 changes: 74 additions & 10 deletions routes/connectors-home/stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import { useSelect } from '@wordpress/data';
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
// eslint-disable-next-line @wordpress/use-recommended-components
import { Notice } from '@wordpress/ui';

/**
* Internal dependencies
*/
import './style.scss';
import { AiPluginCallout } from './ai-plugin-callout';
import { registerDefaultConnectors } from './default-connectors';
import {
getIsFileModDisabled,
registerDefaultConnectors,
} from './default-connectors';
import { unlock } from '../lock-unlock';

const { store } = unlock( connectorsPrivateApis );
Expand All @@ -31,20 +36,62 @@ const { store } = unlock( connectorsPrivateApis );
registerDefaultConnectors();

function ConnectorsPage() {
const { connectors, canInstallPlugins } = useSelect(
( select ) => ( {
connectors: unlock( select( store ) ).getConnectors(),
canInstallPlugins: select( coreStore ).canUser( 'create', {
kind: 'root',
name: 'plugin',
} ),
} ),
const isFileModDisabled = getIsFileModDisabled();

const { connectors, canInstallPlugins, isAiPluginInstalled } = useSelect(
( select ) => {
const coreSelect = select( coreStore );
const aiPlugin = coreSelect.getEntityRecord(
'root',
'plugin',
'ai/ai'
);
return {
connectors: unlock( select( store ) ).getConnectors(),
canInstallPlugins: coreSelect.canUser( 'create', {
kind: 'root',
name: 'plugin',
} ),
isAiPluginInstalled: !! aiPlugin,
};
},
[]
);

const renderableConnectors = connectors.filter(
( connector: ConnectorConfig ) => connector.render
);
const aiProviderPluginSlugs = Array.from(
new Set(
connectors
.filter(
( connector: ConnectorConfig ) =>
connector.type === 'ai_provider'
)
.map(
( connector: ConnectorConfig ) =>
connector.plugin?.file?.split( '/' )[ 0 ]
)
.filter( ( slug ): slug is string => !! slug )
)
).sort();
const installedPluginSlugs = new Set(
connectors
.filter(
( connector: ConnectorConfig ) => connector.plugin?.isInstalled
)
.map(
( connector: ConnectorConfig ) =>
connector.plugin?.file?.split( '/' )[ 0 ]
)
.filter( ( slug: string | undefined ): slug is string => !! slug )
);
if ( isAiPluginInstalled ) {
installedPluginSlugs.add( 'ai' );
}
const manualInstallPluginSlugs = [ 'ai', ...aiProviderPluginSlugs ].filter(
( slug ) => ! installedPluginSlugs.has( slug )
);
const isEmpty = renderableConnectors.length === 0;

return (
Expand All @@ -59,6 +106,23 @@ function ConnectorsPage() {
isEmpty ? ' connectors-page--empty' : ''
}` }
>
{ manualInstallPluginSlugs.length > 0 &&
( isFileModDisabled || ! canInstallPlugins ) && (
<Notice.Root
intent="info"
className="connectors-page__file-mods-notice"
>
<Notice.Description>
{ isFileModDisabled
? __(
'Plugins cannot be installed here due to your site configuration. Install them manually using your normal deployment workflow.'
)
: __(
'You do not have permission to install plugins. Please ask a site administrator to install them for you.'
) }
</Notice.Description>
</Notice.Root>
) }
{ isEmpty ? (
<VStack
alignment="center"
Expand Down Expand Up @@ -109,7 +173,7 @@ function ConnectorsPage() {
</VStack>
</VStack>
) }
{ canInstallPlugins && (
{ canInstallPlugins && ! isFileModDisabled && (
<p>
{ createInterpolateElement(
__(
Expand Down
4 changes: 4 additions & 0 deletions routes/connectors-home/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ $sticky-header-clearance: 120px;
scroll-margin-top: $sticky-header-clearance;
}

&__file-mods-notice {
margin-bottom: 16px;
}

&--empty {
flex-grow: 1;
display: flex;
Expand Down
15 changes: 11 additions & 4 deletions test/e2e/specs/admin/connectors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,13 @@ test.describe( 'Connectors', () => {
slug: 'gutenberg-test-connectors-never-installed',
name: 'Test Install Required Connector',
action: 'Install',
pluginSlug: 'gutenberg-test-connectors-never-installed',
};
const activateRequiredConnector = {
slug: 'hello',
name: 'Test Activate Required Connector',
action: 'Activate',
pluginSlug: 'hello',
};
const clearCapabilityRestriction = async ( requestUtils ) => {
await requestUtils.rest( {
Expand Down Expand Up @@ -519,7 +521,7 @@ test.describe( 'Connectors', () => {
CONNECTORS_PAGE_QUERY
);

for ( const { slug, name, action } of [
for ( const { slug, name, action, pluginSlug } of [
installRequiredConnector,
activateRequiredConnector,
] ) {
Expand All @@ -528,9 +530,14 @@ test.describe( 'Connectors', () => {
await expect(
card.getByRole( 'heading', { name, level: 2 } )
).toBeVisible();
await expect(
card.getByText( 'Not available' )
).toBeVisible();
const learnMoreLink = card.getByRole( 'link', {
name: 'Learn more',
} );
await expect( learnMoreLink ).toBeVisible();
await expect( learnMoreLink ).toHaveAttribute(
'href',
`https://wordpress.org/plugins/${ pluginSlug }/`
);
await expect(
card.getByRole( 'button', { name: action } )
).toBeHidden();
Expand Down
Loading