From d04caf2d1831a82dd336442e8766d822a9a92c28 Mon Sep 17 00:00:00 2001 From: harshit Date: Tue, 11 Aug 2026 01:14:19 +0530 Subject: [PATCH 1/4] feat(dashboard): add recommended plugins in performance tab --- .../src/Components/Content/Settings.js | 45 +++--- .../Content/Settings/PerformancePlugins.js | 139 ++++++++++++++++++ 2 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js diff --git a/assets/apps/dashboard/src/Components/Content/Settings.js b/assets/apps/dashboard/src/Components/Content/Settings.js index 075fafa611..99a022da7d 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings.js +++ b/assets/apps/dashboard/src/Components/Content/Settings.js @@ -13,6 +13,7 @@ import { import { useSelect } from '@wordpress/data'; import Card from '../../Layout/Card'; +import PerformancePlugins from './Settings/PerformancePlugins'; import { NEVE_HAS_PRO, NEVE_SHOW_WHITELABEL, @@ -139,28 +140,36 @@ const Settings = () => {
- - {tab === 'general' && ( - - - - )} +
+ + {tab === 'general' && ( + + + + )} + {tab === 'performance' && ( + + + + )} + {tab === 'white-label' && ( + + + + )} + {tab === 'manage-modules' && ( + + + + )} + + {tab === 'performance' && ( - + )} - {tab === 'white-label' && ( - - - - )} - {tab === 'manage-modules' && ( - - - - )} - +
); }; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js new file mode 100644 index 0000000000..f2bc2d4f39 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js @@ -0,0 +1,139 @@ +/* global neveDash */ +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import cn from 'classnames'; +import { LoaderCircle, LucidePuzzle, LucideRocket } from 'lucide-react'; + +import Pill from '../../Common/Pill'; +import Toast from '../../Common/Toast'; +import TransitionInOut from '../../Common/TransitionInOut'; +import Card from '../../../Layout/Card'; +import usePluginActions from '../../../Hooks/usePluginActions'; +import { + NEVE_HIDE_PLUGINS, + NEVE_PLUGIN_ICON_MAP, + NEVE_STORE, +} from '../../../utils/constants'; + +const PROMOTED_SLUGS = ['optimole-wp', 'wp-cloudflare-page-cache']; + +const PluginCard = ({ slug }) => { + const ICON = NEVE_PLUGIN_ICON_MAP[slug] || LucidePuzzle; + // Titles and descriptions are already localized in inc/admin/dashboard/main.php. + const { title, description } = neveDash.plugins[slug]; + + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const { doPluginAction, loading, buttonText } = usePluginActions( + slug, + true + ); + + const isPluginActive = useSelect( + (select) => select(NEVE_STORE).getPlugins()[slug]?.cta === 'deactivate' + ); + + if (isPluginActive && !success) { + return null; + } + + const handleClick = async () => { + setError(null); + + window.tiTrk?.with('neve').set(slug, { + feature: 'performance-plugin-promo', + featureComponent: slug, + }); + + const result = await doPluginAction(); + + if (result.success) { + setSuccess(true); + + return; + } + + setError(result.error); + }; + + return ( +
+
+ +

{title}

+ + {success && ( +
+ + {__('Active', 'neve')} + +
+ )} +
+ + {/* grow keeps the CTAs aligned when descriptions wrap to different heights */} +

+ {description} +

+ + {!success && ( + + )} + + {error && ( +
+ +
+ )} +
+ ); +}; + +const PerformancePlugins = () => { + const plugins = neveDash.plugins || {}; + + // A promoted plugin can be absent entirely, e.g. Super Page Cache is dropped when SPC Pro is installed. + const availableSlugs = PROMOTED_SLUGS.filter( + (slug) => plugins[slug] && plugins[slug].cta !== 'deactivate' + ); + + if (NEVE_HIDE_PLUGINS || availableSlugs.length < 1) { + return null; + } + + return ( + } + title={__('Recommended Plugins', 'neve')} + > +
1, + })} + > + {availableSlugs.map((slug) => ( + + ))} +
+
+ ); +}; + +export default PerformancePlugins; From 5d0663dd1cf0901cc2aedc77da05802e14554c35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:10:52 +0000 Subject: [PATCH 2/4] fix: hide promoted plugin cards after successful activation Co-authored-by: harshitarora-in <56164789+harshitarora-in@users.noreply.github.com> --- .../Content/Settings/PerformancePlugins.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js index f2bc2d4f39..9c03d81695 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js @@ -1,6 +1,6 @@ /* global neveDash */ import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import cn from 'classnames'; import { LoaderCircle, LucidePuzzle, LucideRocket } from 'lucide-react'; @@ -35,6 +35,18 @@ const PluginCard = ({ slug }) => { (select) => select(NEVE_STORE).getPlugins()[slug]?.cta === 'deactivate' ); + useEffect(() => { + if (!success) { + return; + } + + const timeoutId = window.setTimeout(() => { + setSuccess(false); + }, 1500); + + return () => window.clearTimeout(timeoutId); + }, [success]); + if (isPluginActive && !success) { return null; } @@ -107,7 +119,7 @@ const PluginCard = ({ slug }) => { }; const PerformancePlugins = () => { - const plugins = neveDash.plugins || {}; + const plugins = useSelect((select) => select(NEVE_STORE).getPlugins(), []); // A promoted plugin can be absent entirely, e.g. Super Page Cache is dropped when SPC Pro is installed. const availableSlugs = PROMOTED_SLUGS.filter( From e5ac5bcd8256085edc255daf3b370ddf39b4ebf3 Mon Sep 17 00:00:00 2001 From: harshit Date: Tue, 18 Aug 2026 20:56:41 +0530 Subject: [PATCH 3/4] refactor(dashboard): render performance plugins inside the tab content Move PerformancePlugins into PerformanceTabContent so it reuses the tab's existing conditional and TransitionWrapper, instead of duplicating both in Settings.js. Nested inside the settings card, the component's own Card rendered a white box inside a white box, so add a `flat` variant to Card that keeps the header but drops the box styling. It defaults to false, leaving the other 20 usages unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- assets/apps/dashboard/src/Components/Content/Settings.js | 7 ------- .../src/Components/Content/Settings/PerformancePlugins.js | 2 ++ .../Components/Content/Settings/PerformanceTabContent.js | 3 +++ assets/apps/dashboard/src/Layout/Card.js | 6 ++++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/assets/apps/dashboard/src/Components/Content/Settings.js b/assets/apps/dashboard/src/Components/Content/Settings.js index 99a022da7d..dfe99f213d 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings.js +++ b/assets/apps/dashboard/src/Components/Content/Settings.js @@ -13,7 +13,6 @@ import { import { useSelect } from '@wordpress/data'; import Card from '../../Layout/Card'; -import PerformancePlugins from './Settings/PerformancePlugins'; import { NEVE_HAS_PRO, NEVE_SHOW_WHITELABEL, @@ -163,12 +162,6 @@ const Settings = () => { )}
- - {tab === 'performance' && ( - - - - )} ); diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js index 9c03d81695..95456495d8 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js @@ -132,6 +132,8 @@ const PerformancePlugins = () => { return ( } title={__('Recommended Plugins', 'neve')} > diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js index 1efe58f04e..91219f1188 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js @@ -11,6 +11,7 @@ import ToggleControl from '../../Controls/ToggleControl'; import Button from '../../Common/Button'; import useLicenseData from '../../../Hooks/useLicenseData'; import OptionGroup from './OptionGroup'; +import PerformancePlugins from './PerformancePlugins'; const LOCAL_HOSTING_OPTION = 'enable_local_fonts'; @@ -115,6 +116,8 @@ export default () => { {(isLicenseValid && ) || } + + ); }; diff --git a/assets/apps/dashboard/src/Layout/Card.js b/assets/apps/dashboard/src/Layout/Card.js index d00980209e..186b842050 100644 --- a/assets/apps/dashboard/src/Layout/Card.js +++ b/assets/apps/dashboard/src/Layout/Card.js @@ -7,12 +7,14 @@ export default ({ afterTitle, className = '', id = null, + flat = false, }) => { + // `flat` keeps the header styling but drops the box, for cards nested inside another one. const classes = cn( [ - 'p-6 rounded-lg shadow-sm', { - 'bg-white': !className.includes('bg-'), + 'p-6 rounded-lg shadow-sm': !flat, + 'bg-white': !flat && !className.includes('bg-'), }, ], className From 01a2a336eaf21a12e599dd26a7116c4d5ccf288c Mon Sep 17 00:00:00 2001 From: harshit Date: Fri, 28 Aug 2026 00:46:58 +0530 Subject: [PATCH 4/4] refactor(dashboard): use ControlWrap for the recommended plugins section Render the promoted plugins block with the existing ControlWrap section primitive instead of a nested Card. Settings.js and Layout/Card.js return to their original state; the section spacing and divider now come from the same rhythm the other settings sections use. PerformancePlugins owns the visible list and shrinks it when a card is dismissed, so the section unmounts once the last card is gone. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/Components/Content/Settings.js | 46 ++++++------ .../Content/Settings/PerformancePlugins.js | 70 +++++++++++-------- .../Content/Settings/PerformanceTabContent.js | 4 +- assets/apps/dashboard/src/Layout/Card.js | 6 +- 4 files changed, 65 insertions(+), 61 deletions(-) diff --git a/assets/apps/dashboard/src/Components/Content/Settings.js b/assets/apps/dashboard/src/Components/Content/Settings.js index dfe99f213d..075fafa611 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings.js +++ b/assets/apps/dashboard/src/Components/Content/Settings.js @@ -139,30 +139,28 @@ const Settings = () => {
-
- - {tab === 'general' && ( - - - - )} - {tab === 'performance' && ( - - - - )} - {tab === 'white-label' && ( - - - - )} - {tab === 'manage-modules' && ( - - - - )} - -
+ + {tab === 'general' && ( + + + + )} + {tab === 'performance' && ( + + + + )} + {tab === 'white-label' && ( + + + + )} + {tab === 'manage-modules' && ( + + + + )} + ); }; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js index 95456495d8..4e59635aee 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformancePlugins.js @@ -1,5 +1,4 @@ /* global neveDash */ -import { useSelect } from '@wordpress/data'; import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import cn from 'classnames'; @@ -8,20 +7,22 @@ import { LoaderCircle, LucidePuzzle, LucideRocket } from 'lucide-react'; import Pill from '../../Common/Pill'; import Toast from '../../Common/Toast'; import TransitionInOut from '../../Common/TransitionInOut'; -import Card from '../../../Layout/Card'; +import ControlWrap from '../../Controls/ControlWrap'; import usePluginActions from '../../../Hooks/usePluginActions'; import { NEVE_HIDE_PLUGINS, NEVE_PLUGIN_ICON_MAP, - NEVE_STORE, } from '../../../utils/constants'; const PROMOTED_SLUGS = ['optimole-wp', 'wp-cloudflare-page-cache']; -const PluginCard = ({ slug }) => { +// How long the "Active" pill stays up before the card removes itself. +const ACTIVE_PILL_TIMEOUT = 1500; + +const PluginCard = ({ slug, data, onDismiss }) => { const ICON = NEVE_PLUGIN_ICON_MAP[slug] || LucidePuzzle; // Titles and descriptions are already localized in inc/admin/dashboard/main.php. - const { title, description } = neveDash.plugins[slug]; + const { title, description } = data; const [error, setError] = useState(null); const [success, setSuccess] = useState(false); @@ -31,26 +32,20 @@ const PluginCard = ({ slug }) => { true ); - const isPluginActive = useSelect( - (select) => select(NEVE_STORE).getPlugins()[slug]?.cta === 'deactivate' - ); - useEffect(() => { if (!success) { return; } + // Only `success` belongs in the deps: onDismiss is recreated on every + // parent render and would restart a countdown already in progress. const timeoutId = window.setTimeout(() => { - setSuccess(false); - }, 1500); + onDismiss(slug); + }, ACTIVE_PILL_TIMEOUT); return () => window.clearTimeout(timeoutId); }, [success]); - if (isPluginActive && !success) { - return null; - } - const handleClick = async () => { setError(null); @@ -77,7 +72,7 @@ const PluginCard = ({ slug }) => { >
-

{title}

+

{title}

{success && (
@@ -119,34 +114,47 @@ const PluginCard = ({ slug }) => { }; const PerformancePlugins = () => { - const plugins = useSelect((select) => select(NEVE_STORE).getPlugins(), []); - - // A promoted plugin can be absent entirely, e.g. Super Page Cache is dropped when SPC Pro is installed. - const availableSlugs = PROMOTED_SLUGS.filter( - (slug) => plugins[slug] && plugins[slug].cta !== 'deactivate' + // Snapshot: a card leaves this list when it asks to, not when the store + // changes, so the "Active" pill outlives the activation request. + const [visibleSlugs, setVisibleSlugs] = useState(() => + PROMOTED_SLUGS.filter((slug) => { + // Super Page Cache is dropped server side when SPC Pro is + // installed, so a promoted slug can be missing entirely. + const plugin = neveDash.plugins[slug]; + + return plugin && plugin.cta !== 'deactivate'; + }) ); - if (NEVE_HIDE_PLUGINS || availableSlugs.length < 1) { + if (NEVE_HIDE_PLUGINS || visibleSlugs.length < 1) { return null; } + const handleDismiss = (dismissed) => + setVisibleSlugs((current) => + current.filter((slug) => slug !== dismissed) + ); + return ( - } - title={__('Recommended Plugins', 'neve')} +
1, + 'md:grid-cols-2': visibleSlugs.length > 1, })} > - {availableSlugs.map((slug) => ( - + {visibleSlugs.map((slug) => ( + ))}
-
+ ); }; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js index 91219f1188..3207a74e19 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js @@ -115,9 +115,9 @@ export default () => { /> {(isLicenseValid && ) || } -
- + +
); }; diff --git a/assets/apps/dashboard/src/Layout/Card.js b/assets/apps/dashboard/src/Layout/Card.js index 186b842050..d00980209e 100644 --- a/assets/apps/dashboard/src/Layout/Card.js +++ b/assets/apps/dashboard/src/Layout/Card.js @@ -7,14 +7,12 @@ export default ({ afterTitle, className = '', id = null, - flat = false, }) => { - // `flat` keeps the header styling but drops the box, for cards nested inside another one. const classes = cn( [ + 'p-6 rounded-lg shadow-sm', { - 'p-6 rounded-lg shadow-sm': !flat, - 'bg-white': !flat && !className.includes('bg-'), + 'bg-white': !className.includes('bg-'), }, ], className