diff --git a/client/dashboard/app/loading-screen.scss b/client/dashboard/app/loading-screen.scss
index 253b29252289..b4fa80fa48bf 100644
--- a/client/dashboard/app/loading-screen.scss
+++ b/client/dashboard/app/loading-screen.scss
@@ -1,17 +1,4 @@
-@keyframes logo-pulse {
- 0% {
- opacity: 0.65;
- filter: brightness(0.85);
- }
- 50% {
- opacity: 1;
- filter: brightness(1.15);
- }
- 100% {
- opacity: 0.65;
- filter: brightness(0.85);
- }
-}
+@import './pulse';
.wpcom-site__logo {
position: absolute;
@@ -20,5 +7,5 @@
transform: translate(-50%, -50%);
margin: 0;
fill: $gray-900;
- animation: logo-pulse 1.5s ease-in-out infinite;
+ animation: pulse 1.5s ease-in-out infinite;
}
diff --git a/client/dashboard/app/pulse.scss b/client/dashboard/app/pulse.scss
new file mode 100644
index 000000000000..6b7637096b0b
--- /dev/null
+++ b/client/dashboard/app/pulse.scss
@@ -0,0 +1,14 @@
+@keyframes pulse {
+ 0% {
+ opacity: 0.65;
+ filter: brightness(0.85);
+ }
+ 50% {
+ opacity: 1;
+ filter: brightness(1.15);
+ }
+ 100% {
+ opacity: 0.65;
+ filter: brightness(0.85);
+ }
+}
diff --git a/client/dashboard/app/router/plugins.tsx b/client/dashboard/app/router/plugins.tsx
index b3b2e2f64573..0b920e65e579 100644
--- a/client/dashboard/app/router/plugins.tsx
+++ b/client/dashboard/app/router/plugins.tsx
@@ -82,7 +82,7 @@ export const pluginRoute = createRoute( {
path: '$pluginId',
loader: async () => {
queryClient.ensureQueryData( marketplacePluginsQuery() );
- await queryClient.ensureQueryData( pluginsQuery() );
+ queryClient.ensureQueryData( pluginsQuery() );
},
} ).lazy( () =>
import( '../../plugins/plugin' ).then( ( d ) =>
diff --git a/client/dashboard/plugins/plugin/index.tsx b/client/dashboard/plugins/plugin/index.tsx
index afe80fd90aec..fb9af39c4ae7 100644
--- a/client/dashboard/plugins/plugin/index.tsx
+++ b/client/dashboard/plugins/plugin/index.tsx
@@ -12,6 +12,8 @@ import { SitesWithThisPlugin } from './sites-with-this-plugin';
import { SitesWithoutThisPlugin } from './sites-without-this-plugin';
import { usePlugin } from './use-plugin';
+import './style.scss';
+
export default function Plugin() {
const { pluginId: pluginSlug } = pluginRoute.useParams();
const { icon, isLoading, sitesWithThisPlugin, plugin } = usePlugin( pluginSlug );
@@ -27,6 +29,14 @@ export default function Plugin() {
);
}
+ let decoration = null;
+
+ if ( icon ) {
+ decoration =
;
+ } else if ( isLoading ) {
+ decoration =
;
+ }
+
return (
}
- decoration={ icon &&
}
+ decoration={ decoration }
title={
plugin ? (
// @ts-expect-error: Can only set one of `children` or `props.dangerouslySetInnerHTML`.
diff --git a/client/dashboard/plugins/plugin/style.scss b/client/dashboard/plugins/plugin/style.scss
new file mode 100644
index 000000000000..55b160adb792
--- /dev/null
+++ b/client/dashboard/plugins/plugin/style.scss
@@ -0,0 +1,10 @@
+@import '../../app/pulse';
+
+.plugin-icon-placeholder {
+ width: 40px;
+ height: 40px;
+ background-color: var(--color-neutral-10, #c3c4c7);
+ border-radius: 4px;
+ animation: pulse 1.4s ease-in-out infinite;
+ will-change: opacity, transform;
+}
diff --git a/client/dashboard/plugins/plugin/use-plugin.ts b/client/dashboard/plugins/plugin/use-plugin.ts
index 64c9c55f36f0..a6e4d5799618 100644
--- a/client/dashboard/plugins/plugin/use-plugin.ts
+++ b/client/dashboard/plugins/plugin/use-plugin.ts
@@ -1,4 +1,4 @@
-import { PluginItem, Site, SitePlugin } from '@automattic/api-core';
+import { MarketplaceSearch, PluginItem, Site, SitePlugin } from '@automattic/api-core';
import {
pluginsQuery,
wpOrgPluginQuery,
@@ -15,8 +15,25 @@ export interface SiteWithPluginData extends Site {
isPluginActive: boolean;
}
+/**
+ * Search for an icon on the cached marketplace search query results for a given plugin slug.
+ */
+const useMarketplaceSearchIcon = ( pluginSlug: string ) => {
+ const queryClient = useQueryClient();
+ const marketplaceSearchPluginData = queryClient
+ .getQueriesData< MarketplaceSearch >( {
+ queryKey: [ 'marketplace-search' ],
+ predicate: ( query ) => query.queryKey.includes( pluginSlug ),
+ } )
+ .flatMap( ( [ , data ] ) => data?.data.results || [] )
+ .find( ( result ) => result.fields.slug === pluginSlug );
+
+ return marketplaceSearchPluginData?.fields.plugin.icons;
+};
+
export const usePlugin = ( pluginSlug: string ) => {
const queryClient = useQueryClient();
+ const availableIcon = useMarketplaceSearchIcon( pluginSlug );
const { queries } = useAppContext();
const locale = useLocale();
const {
@@ -29,9 +46,10 @@ export const usePlugin = ( pluginSlug: string ) => {
marketplacePluginsQuery()
);
const isMarketplacePlugin = !! marketplacePlugins?.results[ pluginSlug ];
- const { data: wpOrgPlugin, isLoading: isLoadingWpOrgPlugin } = useQuery(
- wpOrgPluginQuery( pluginSlug, locale )
- );
+ const { data: wpOrgPlugin, isLoading: isLoadingWpOrgPlugin } = useQuery( {
+ ...wpOrgPluginQuery( pluginSlug, locale ),
+ enabled: ! availableIcon,
+ } );
// Query needed to get the action_links
const sitePluginQueryResults = useQueries( {
queries: Object.keys( sitesPlugins?.sites || {} ).map( ( id ) =>
@@ -90,7 +108,9 @@ export const usePlugin = ( pluginSlug: string ) => {
: [ [], [] ];
let icon;
- if ( isMarketplacePlugin ) {
+ if ( availableIcon ) {
+ icon = availableIcon;
+ } else if ( isMarketplacePlugin ) {
icon = marketplacePlugins?.results[ pluginSlug ]?.icons;
} else if ( wpOrgPlugin?.icons ) {
if ( '1x' in wpOrgPlugin.icons ) {
diff --git a/packages/api-queries/src/marketplace-search.ts b/packages/api-queries/src/marketplace-search.ts
index 27ad3536a58c..a35c7293f8de 100644
--- a/packages/api-queries/src/marketplace-search.ts
+++ b/packages/api-queries/src/marketplace-search.ts
@@ -9,7 +9,7 @@ export const marketplaceSearchQuery = ( {
slugs: string[];
} ) =>
queryOptions( {
- queryKey: [ 'marketplace-search', slugs, perPage ],
+ queryKey: [ 'marketplace-search', ...slugs, perPage ],
queryFn: () =>
fetchMarketplaceSearch( {
category: 'all',