Skip to content

Commit acfced4

Browse files
authored
MSD Plugins: Available on section (#107588)
1 parent 955dfc5 commit acfced4

File tree

8 files changed

+460
-85
lines changed

8 files changed

+460
-85
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
__experimentalHStack as HStack,
3+
__experimentalVStack as VStack,
4+
} from '@wordpress/components';
5+
import { Name, URL, SiteIconLink, SiteLink } from '../../../sites/site-fields';
6+
import type { Site } from '@automattic/api-core';
7+
8+
type PluginSiteFieldContentProps< T extends Site > = {
9+
site: T;
10+
name: string;
11+
url: string;
12+
};
13+
14+
export function PluginSiteFieldContent< T extends Site >( {
15+
site,
16+
name,
17+
url,
18+
}: PluginSiteFieldContentProps< T > ) {
19+
return (
20+
<HStack spacing={ 3 } alignment="center" justify="flex-start">
21+
<SiteIconLink site={ site } />
22+
<VStack spacing={ 0 } alignment="flex-start">
23+
<SiteLink site={ site }>
24+
<Name site={ site } value={ name } />
25+
</SiteLink>
26+
<URL site={ site } value={ url } />
27+
</VStack>
28+
</HStack>
29+
);
30+
}
Lines changed: 109 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { __experimentalVStack as VStack } from '@wordpress/components';
1+
import { type PluginItem, Site } from '@automattic/api-core';
2+
import { __experimentalVStack as VStack, privateApis } from '@wordpress/components';
23
import { __, _n, sprintf } from '@wordpress/i18n';
4+
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
5+
import { useMemo, useState } from 'react';
36
import Breadcrumbs from '../../app/breadcrumbs';
47
import { pluginRoute } from '../../app/router/plugins';
58
import { DataViewsCard } from '../../components/dataviews';
@@ -10,13 +13,107 @@ import { Text } from '../../components/text';
1013
import { TextBlur } from '../../components/text-blur';
1114
import { SitesWithThisPlugin } from './sites-with-this-plugin';
1215
import { SitesWithoutThisPlugin } from './sites-without-this-plugin';
13-
import { usePlugin } from './use-plugin';
16+
import { SiteWithPluginData, usePlugin } from './use-plugin';
1417

1518
import './style.scss';
1619

20+
const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
21+
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
22+
'@wordpress/components'
23+
);
24+
25+
const { Tabs } = unlock( privateApis );
26+
27+
type PluginTabsProps = {
28+
pluginSlug: string;
29+
isLoading: boolean;
30+
sitesWithThisPlugin: SiteWithPluginData[];
31+
sitesWithoutThisPlugin: Site[];
32+
plugin: PluginItem | undefined;
33+
pluginName?: string;
34+
pluginBySiteId: Map< number, PluginItem >;
35+
};
36+
37+
function PluginTabs( {
38+
pluginSlug,
39+
isLoading,
40+
sitesWithThisPlugin,
41+
sitesWithoutThisPlugin,
42+
plugin,
43+
pluginName,
44+
pluginBySiteId,
45+
}: PluginTabsProps ) {
46+
const [ activeTab, setActiveTab ] = useState< 'installed' | 'available' >( 'installed' );
47+
48+
return (
49+
<VStack spacing={ 6 }>
50+
<Tabs
51+
selectedTabId={ activeTab }
52+
onSelect={ ( tabId: 'installed' | 'available' ) => setActiveTab( tabId ) }
53+
>
54+
<Tabs.TabList>
55+
<Tabs.Tab tabId="installed">
56+
<SectionHeader
57+
level={ 3 }
58+
title={ sprintf(
59+
// translators: %(count) is the number of sites the plugin is installed on.
60+
_n(
61+
'Installed on %(count)d site',
62+
'Installed on %(count)d sites',
63+
sitesWithThisPlugin.length
64+
),
65+
{ count: sitesWithThisPlugin.length }
66+
) }
67+
/>
68+
</Tabs.Tab>
69+
<Tabs.Tab tabId="available">
70+
<SectionHeader level={ 3 } title={ __( 'Available on' ) } />
71+
</Tabs.Tab>
72+
</Tabs.TabList>
73+
74+
<Tabs.TabPanel tabId="installed">
75+
<VStack spacing={ 6 }>
76+
<DataViewsCard>
77+
<SitesWithThisPlugin
78+
pluginSlug={ pluginSlug }
79+
isLoading={ isLoading }
80+
plugin={ plugin }
81+
pluginBySiteId={ pluginBySiteId }
82+
sitesWithThisPlugin={ sitesWithThisPlugin }
83+
/>
84+
</DataViewsCard>
85+
</VStack>
86+
</Tabs.TabPanel>
87+
88+
<Tabs.TabPanel tabId="available">
89+
<VStack spacing={ 6 }>
90+
<DataViewsCard>
91+
<SitesWithoutThisPlugin
92+
pluginSlug={ pluginSlug }
93+
pluginName={ pluginName }
94+
isLoading={ isLoading }
95+
sitesWithoutThisPlugin={ sitesWithoutThisPlugin }
96+
/>
97+
</DataViewsCard>
98+
</VStack>
99+
</Tabs.TabPanel>
100+
</Tabs>
101+
</VStack>
102+
);
103+
}
104+
17105
export default function Plugin() {
18106
const { pluginId: pluginSlug } = pluginRoute.useParams();
19-
const { icon, isLoading, sitesWithThisPlugin, plugin } = usePlugin( pluginSlug );
107+
const { icon, isLoading, plugin, pluginBySiteId, sitesWithThisPlugin, sitesWithoutThisPlugin } =
108+
usePlugin( pluginSlug );
109+
110+
const decoration = useMemo( () => {
111+
if ( icon ) {
112+
return <img src={ icon } alt={ plugin?.name } />;
113+
} else if ( isLoading ) {
114+
return <div className="plugin-icon-placeholder" aria-hidden="true" />;
115+
}
116+
}, [ icon, isLoading, plugin?.name ] );
20117

21118
if ( ! isLoading && ! plugin ) {
22119
return (
@@ -29,14 +126,6 @@ export default function Plugin() {
29126
);
30127
}
31128

32-
let decoration = null;
33-
34-
if ( icon ) {
35-
decoration = <img src={ icon } alt={ plugin?.name } />;
36-
} else if ( isLoading ) {
37-
decoration = <div className="plugin-icon-placeholder" aria-hidden="true" />;
38-
}
39-
40129
return (
41130
<PageLayout
42131
size="large"
@@ -58,33 +147,15 @@ export default function Plugin() {
58147
</VStack>
59148
}
60149
>
61-
<VStack spacing={ 20 }>
62-
<VStack spacing={ 6 }>
63-
<SectionHeader
64-
title={ sprintf(
65-
// translators: %(count) is the number of sites the plugin is installed on.
66-
_n(
67-
'Installed on %(count)d site',
68-
'Installed on %(count)d sites',
69-
sitesWithThisPlugin.length
70-
),
71-
{ count: sitesWithThisPlugin.length }
72-
) }
73-
/>
74-
75-
<DataViewsCard>
76-
<SitesWithThisPlugin pluginSlug={ pluginSlug } />
77-
</DataViewsCard>
78-
</VStack>
79-
80-
<VStack spacing={ 6 }>
81-
<SectionHeader title={ __( 'Available on' ) } />
82-
83-
<DataViewsCard>
84-
<SitesWithoutThisPlugin pluginSlug={ pluginSlug } />
85-
</DataViewsCard>
86-
</VStack>
87-
</VStack>
150+
<PluginTabs
151+
pluginSlug={ pluginSlug }
152+
isLoading={ isLoading }
153+
plugin={ plugin }
154+
pluginName={ plugin?.name }
155+
pluginBySiteId={ pluginBySiteId }
156+
sitesWithThisPlugin={ sitesWithThisPlugin }
157+
sitesWithoutThisPlugin={ sitesWithoutThisPlugin }
158+
/>
88159
</PageLayout>
89160
);
90161
}

client/dashboard/plugins/plugin/sites-with-this-plugin.tsx

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ import {
88
sitePluginUpdateMutation,
99
} from '@automattic/api-queries';
1010
import { useMutation } from '@tanstack/react-query';
11-
import {
12-
__experimentalText as Text,
13-
Button,
14-
Icon,
15-
__experimentalHStack as HStack,
16-
__experimentalVStack as VStack,
17-
} from '@wordpress/components';
11+
import { __experimentalText as Text, Button, Icon } from '@wordpress/components';
1812
import {
1913
DataViews,
2014
filterSortAndPaginate,
@@ -25,7 +19,6 @@ import {
2519
import { __, sprintf } from '@wordpress/i18n';
2620
import { link, linkOff, trash } from '@wordpress/icons';
2721
import { useCallback, useMemo, useState } from 'react';
28-
import { Name, URL, SiteIconLink, SiteLink } from '../../sites/site-fields';
2922
import { getSiteDisplayName } from '../../utils/site-name';
3023
import { getSiteDisplayUrl } from '../../utils/site-url';
3124
import ActionRenderModal, { getModalHeader } from '../manage/components/action-render-modal';
@@ -35,10 +28,12 @@ import { getViewFilteredByUpdates } from '../utils/update-filters';
3528
import { ActionRenderModalWrapper } from './components/action-render-modal-wrapper';
3629
import { ActiveToggle } from './components/active-toggle';
3730
import { AutoupdateToggle } from './components/autoupdate-toggle';
38-
import { SiteWithPluginData, usePlugin } from './use-plugin';
31+
import { PluginSiteFieldContent } from './components/plugin-site-field-content';
32+
import { SiteWithPluginData } from './use-plugin';
3933
import { getAllowedPluginActions } from './utils/get-allowed-plugin-actions';
4034
import { mapToPluginListRow } from './utils/map-to-plugin-list-row';
4135
import type { PluginListRow } from '../manage/types';
36+
import type { PluginItem } from '@automattic/api-core';
4237

4338
const defaultView: View = {
4439
type: 'table',
@@ -48,10 +43,23 @@ const defaultView: View = {
4843
perPage: 10,
4944
};
5045

51-
export const SitesWithThisPlugin = ( { pluginSlug }: { pluginSlug: string } ) => {
46+
type SitesWithThisPluginProps = {
47+
pluginSlug: string;
48+
isLoading: boolean;
49+
plugin: PluginItem | undefined;
50+
pluginBySiteId: Map< number, PluginItem >;
51+
sitesWithThisPlugin: SiteWithPluginData[];
52+
};
53+
54+
export const SitesWithThisPlugin = ( {
55+
pluginSlug,
56+
isLoading,
57+
plugin,
58+
pluginBySiteId,
59+
sitesWithThisPlugin,
60+
}: SitesWithThisPluginProps ) => {
5261
const { mutateAsync } = useMutation( sitePluginUpdateMutation() );
5362
const updateAction = buildBulkSitesPluginAction( mutateAsync );
54-
const { isLoading, plugin, pluginBySiteId, sitesWithThisPlugin } = usePlugin( pluginSlug );
5563
const [ view, setView ] = useState< View >( defaultView );
5664
const { mutateAsync: activateMutate } = useMutation( sitePluginActivateMutation() );
5765
const { mutateAsync: deactivateMutate } = useMutation( sitePluginDeactivateMutation() );
@@ -84,15 +92,11 @@ export const SitesWithThisPlugin = ( { pluginSlug }: { pluginSlug: string } ) =>
8492
type: 'text',
8593
getValue: ( { item }: { item: SiteWithPluginData } ) => getSiteDisplayName( item ),
8694
render: ( { field, item } ) => (
87-
<HStack spacing={ 3 } alignment="center" justify="flex-start">
88-
<SiteIconLink site={ item } />
89-
<VStack spacing={ 0 } alignment="flex-start">
90-
<SiteLink site={ item }>
91-
<Name site={ item } value={ field.getValue( { item } ) } />
92-
</SiteLink>
93-
<URL site={ item } value={ getSiteDisplayUrl( item ) } />
94-
</VStack>
95-
</HStack>
95+
<PluginSiteFieldContent
96+
site={ item }
97+
name={ field.getValue( { item } ) as string }
98+
url={ getSiteDisplayUrl( item ) }
99+
/>
96100
),
97101
enableHiding: false,
98102
enableSorting: true,
@@ -395,9 +399,15 @@ export const SitesWithThisPlugin = ( { pluginSlug }: { pluginSlug: string } ) =>
395399
label: __( 'WP Admin ↗' ),
396400
callback: ( items ) => {
397401
const [ site ] = items;
398-
window.open( site.actionLinks?.Settings, '_blank' );
402+
403+
if ( ! site?.URL ) {
404+
return;
405+
}
406+
407+
const baseUrl = site.URL.replace( /\/$/, '' );
408+
window.open( `${ baseUrl }/wp-admin/plugins.php`, '_blank' );
399409
},
400-
isEligible: ( item ) => !! item.actionLinks?.Settings,
410+
isEligible: ( item ) => !! item.URL,
401411
supportsBulk: false,
402412
isPrimary: true,
403413
},

0 commit comments

Comments
 (0)