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' ;
23import { __ , _n , sprintf } from '@wordpress/i18n' ;
4+ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis' ;
5+ import { useMemo , useState } from 'react' ;
36import Breadcrumbs from '../../app/breadcrumbs' ;
47import { pluginRoute } from '../../app/router/plugins' ;
58import { DataViewsCard } from '../../components/dataviews' ;
@@ -10,13 +13,107 @@ import { Text } from '../../components/text';
1013import { TextBlur } from '../../components/text-blur' ;
1114import { SitesWithThisPlugin } from './sites-with-this-plugin' ;
1215import { SitesWithoutThisPlugin } from './sites-without-this-plugin' ;
13- import { usePlugin } from './use-plugin' ;
16+ import { SiteWithPluginData , usePlugin } from './use-plugin' ;
1417
1518import './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+
17105export 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}
0 commit comments