Skip to content

Commit 6464e82

Browse files
kaeizenbfintal
andauthored
Feat: add Useful Plugins in the admin setting and Cimo download notice in the editor (#3635)
* init * lazy load cimo notice, display notice in image control * code rabbit's qa fixes * fix to coderabbit's qa * Apply suggestion from @bfintal * add useful plugins * remove error logs, add guard if pluginData is not available * fix functions to localize script * check for wp error * fix to coderabbit's qa * minor fix * reset status * tweaked css --------- Co-authored-by: Benjamin Intal <[email protected]> Co-authored-by: [email protected] <>
1 parent 06d1a83 commit 6464e82

File tree

12 files changed

+892
-63
lines changed

12 files changed

+892
-63
lines changed

plugin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ function is_frontend() {
288288
require_once( plugin_dir_path( __FILE__ ) . 'src/plugins/global-settings/block-styles/index.php' );
289289
require_once( plugin_dir_path( __FILE__ ) . 'src/css-optimize.php' );
290290
require_once( plugin_dir_path( __FILE__ ) . 'src/compatibility/index.php' );
291+
291292
if ( ! is_admin() ) {
292293
require_once( plugin_dir_path( __FILE__ ) . 'src/lightbox/index.php' );
293294
require_once( plugin_dir_path( __FILE__ ) . 'src/block/accordion/index.php' );
@@ -314,6 +315,7 @@ function is_frontend() {
314315
*/
315316
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/getting-started.php' );
316317
if ( is_admin() ) {
318+
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/useful-plugins.php' ); // For cross-marketing
317319
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/index.php' );
318320
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/news.php' );
319321
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/freemius.php' );

src/components/image-control2/index.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Button from '../button'
1111
* External dependencies
1212
*/
1313
import classnames from 'classnames'
14-
import { i18n } from 'stackable'
14+
import { i18n, cimo } from 'stackable'
1515
import {
1616
useAttributeName, useBlockAttributesContext, useBlockSetAttributesContext,
1717
} from '~stackable/hooks'
@@ -20,8 +20,11 @@ import {
2020
* WordPress dependencies
2121
*/
2222
import { __ } from '@wordpress/i18n'
23-
import { Fragment, memo } from '@wordpress/element'
23+
import {
24+
Fragment, memo, useEffect, useState,
25+
} from '@wordpress/element'
2426
import { MediaUpload } from '@wordpress/block-editor'
27+
import { currentUserHasCapability } from '~stackable/util'
2528

2629
const ImageControl = memo( props => {
2730
const attrNameId = useAttributeName( `${ props.attribute }Id`, props.responsive, props.hover )
@@ -81,7 +84,37 @@ const ImageControl = memo( props => {
8184
} )
8285
}
8386

84-
return (
87+
const [ CimoDownloadNotice, setCimoDownloadNotice ] = useState( null )
88+
89+
useEffect( () => {
90+
// Skip displaying the Cimo notice if the plugin is already activated or the user has chosen to hide the notice
91+
if ( ! cimo || cimo.hideNotice || cimo.status === 'activated' ) {
92+
return
93+
}
94+
95+
const userCanInstall = currentUserHasCapability( 'install_plugins' )
96+
const userCanActivate = currentUserHasCapability( 'activate_plugins' )
97+
// Show the Cimo notice only if the user has permissions to install or activate plugins
98+
if ( ( cimo.status === 'not_installed' && userCanInstall ) || ( cimo.status === 'installed' && userCanActivate ) ) {
99+
const loadNotice = async () => {
100+
try {
101+
// Import the Cimo notice component with explicit chunk naming
102+
const { default: CimoNoticeComponent } = await import(
103+
/* webpackChunkName: "cimo-download-notice" */
104+
/* webpackMode: "lazy" */
105+
'../../lazy-components/cimo'
106+
)
107+
setCimoDownloadNotice( () => CimoNoticeComponent )
108+
} catch ( err ) {
109+
// eslint-disable-next-line no-console
110+
console.error( 'Failed to load Cimo download notice component:', err )
111+
}
112+
}
113+
loadNotice()
114+
}
115+
}, [] )
116+
117+
return ( <>
85118
<AdvancedControl
86119
{ ...controlProps }
87120
valueCheckAttribute={ props.attribute + 'Url' }
@@ -112,7 +145,7 @@ const ImageControl = memo( props => {
112145
/>
113146
) }
114147
{ type === 'image' && (
115-
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
148+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
116149
<img
117150
className="ugb-image-preview"
118151
draggable="false"
@@ -172,6 +205,8 @@ const ImageControl = memo( props => {
172205
hasPanelModifiedIndicator={ props.hasPanelModifiedIndicator }
173206
/>
174207
</AdvancedControl>
208+
{ CimoDownloadNotice && <CimoDownloadNotice onDismiss={ () => setCimoDownloadNotice( null ) } /> }
209+
</>
175210
)
176211
} )
177212

src/init.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ public function register_block_editor_assets() {
384384
'version' => array_shift( $version_parts ),
385385
'wpVersion' => ! empty( $wp_version ) ? preg_replace( '/-.*/', '', $wp_version ) : $wp_version, // Ensure semver, strip out after dash
386386
'adminUrl' => admin_url(),
387+
'ajaxUrl' => admin_url('admin-ajax.php'),
387388

388389
// Fonts.
389390
'locale' => get_locale(),

src/lazy-components/cimo/index.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import {
2+
cimo, i18n, ajaxUrl,
3+
} from 'stackable'
4+
import { createRoot } from '~stackable/util'
5+
6+
import { __ } from '@wordpress/i18n'
7+
import { Dashicon } from '@wordpress/components'
8+
import domReady from '@wordpress/dom-ready'
9+
import {
10+
useState, useRef, useEffect,
11+
} from '@wordpress/element'
12+
import { models } from '@wordpress/api'
13+
14+
const CimoDownloadNotice = props => {
15+
const [ data, setData ] = useState( { status: cimo?.status, action: cimo?.action } )
16+
const pollCountRef = useRef( 0 )
17+
18+
const onDismiss = () => {
19+
const settings = new models.Settings( { stackable_hide_cimo_notice: true } ) // eslint-disable-line camelcase
20+
settings.save()
21+
22+
if ( cimo ) {
23+
cimo.hideNotice = true
24+
}
25+
26+
// Update the global stackable.cimo hideNotice variable
27+
if ( typeof window !== 'undefined' && window.stackable?.cimo ) {
28+
window.stackable.cimo.hideNotice = true
29+
}
30+
31+
props?.onDismiss?.()
32+
}
33+
34+
// Polls the Cimo plugin status to detect installation or activation state changes
35+
const pollStatus = ( action, link, pollOnce = false ) => {
36+
fetch( ajaxUrl, {
37+
method: 'POST',
38+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
39+
body: new URLSearchParams( {
40+
action: 'stackable_check_cimo_status',
41+
// eslint-disable-next-line camelcase
42+
user_action: action,
43+
nonce: cimo.nonce,
44+
} ),
45+
credentials: 'same-origin',
46+
} ).then( res => res.json() ).then( res => {
47+
if ( ! res.success ) {
48+
setData( { status: 'error', action: '' } )
49+
50+
const errorMessage = res?.data?.message ? res.data.message : 'Server error'
51+
52+
throw new Error( 'Stackable: ' + errorMessage )
53+
}
54+
55+
if ( pollCountRef.current === 0 && link ) {
56+
window.open( link, '_blank' )
57+
}
58+
59+
pollCountRef.current += 1
60+
61+
const _data = res.data
62+
63+
if ( data.status !== _data.status ) {
64+
setData( _data )
65+
66+
// Update the global stackable.cimo status/action variables
67+
// so new image block selections reflect the latest Cimo installation state
68+
if ( typeof window !== 'undefined' && window.stackable?.cimo ) {
69+
window.stackable.cimo.status = _data.status
70+
window.stackable.cimo.action = _data.action
71+
}
72+
}
73+
74+
// Stop polling if it has reached 3 attempts, or plugin status indicates installation/activation is complete
75+
if ( pollOnce || pollCountRef.current >= 3 ||
76+
( action === 'install' && ( _data.status === 'installed' || _data.status === 'activated' ) ) ||
77+
( action === 'activate' && _data.status === 'activated' )
78+
) {
79+
return
80+
}
81+
82+
setTimeout( () => {
83+
pollStatus( action )
84+
}, 3000 * pollCountRef.current )
85+
} ).catch( e => {
86+
// eslint-disable-next-line no-console
87+
console.error( e.message )
88+
} )
89+
}
90+
91+
useEffect( () => {
92+
const _media = wp.media
93+
const old = _media.view.MediaFrame.Select
94+
95+
// When the media library closes, check and update the Cimo plugin status
96+
// to ensure the UI reflects the latest installation or activation state.
97+
_media.view.MediaFrame.Select = old.extend( {
98+
initialize() {
99+
old.prototype.initialize.apply( this, arguments )
100+
101+
this.on( 'close', () => {
102+
pollCountRef.current = 0
103+
if ( data.status === 'activated' ) {
104+
return
105+
}
106+
107+
if ( data.status === 'not_installed' ) {
108+
pollStatus( 'install', null, true )
109+
return
110+
}
111+
112+
pollStatus( 'activate', null, true )
113+
} )
114+
},
115+
} )
116+
}, [] )
117+
118+
const onActionClick = e => {
119+
e.preventDefault()
120+
pollCountRef.current = 0
121+
122+
if ( data.status === 'not_installed' ) {
123+
setData( { status: 'installing', action: '' } )
124+
pollStatus( 'install', e.currentTarget.href )
125+
return
126+
}
127+
128+
setData( { status: 'activating', action: '' } )
129+
pollStatus( 'activate', e.currentTarget.href )
130+
}
131+
132+
return ( <>
133+
<button aria-label="dismiss" onClick={ onDismiss }><Dashicon icon="no" /></button>
134+
{ data.status === 'activated'
135+
? <p>
136+
{ __( 'Cimo Image Optimizer has been activated. Please refresh this page to begin optimizing your images automatically.', i18n ) }
137+
</p>
138+
: <p> { __( 'Instantly optimize images as you upload them with Cimo Image Optimizer.', i18n ) }
139+
&nbsp;
140+
{ data.status === 'installing'
141+
? <span> { __( 'Installing', i18n ) }</span>
142+
: ( data.status === 'activating'
143+
? <span>{ __( 'Activating', i18n ) } </span>
144+
: <a href={ data.action } target="_blank" rel="noreferrer" onClick={ onActionClick }>
145+
{ data.status === 'installed' ? __( 'Activate now', i18n ) : __( 'Install now', i18n ) }
146+
</a>
147+
)
148+
}
149+
</p>
150+
}
151+
</> )
152+
}
153+
154+
const CimoDownloadNoticeWrapper = props => {
155+
return <div className="stk-cimo-notice"> <CimoDownloadNotice { ...props } /> </div>
156+
}
157+
158+
export default CimoDownloadNoticeWrapper
159+
160+
domReady( () => {
161+
if ( ! cimo || cimo.status === 'activated' || cimo.hideNotice ||
162+
typeof wp === 'undefined' || ! wp?.media?.view?.Attachment?.Details
163+
) {
164+
return
165+
}
166+
167+
const CurrentDetailsView = wp.media.view.Attachment.Details
168+
169+
// Display the Cimo download notice in the media library
170+
const CustomDetailsView = CurrentDetailsView.extend( {
171+
render() {
172+
const result = CurrentDetailsView.prototype.render.apply( this, arguments )
173+
174+
if ( cimo?.hideNotice ) {
175+
return result
176+
}
177+
178+
const details = this.el.querySelector( '.attachment-info .details' )
179+
if ( details && ! this.el.querySelector( '.stk-cimo-notice' ) ) {
180+
const noticeDiv = document.createElement( 'div' )
181+
noticeDiv.className = 'stk-cimo-notice'
182+
183+
const onDismiss = () => {
184+
if ( noticeDiv && noticeDiv.parentNode ) {
185+
noticeDiv.parentNode.removeChild( noticeDiv )
186+
}
187+
}
188+
189+
createRoot( noticeDiv ).render( <CimoDownloadNotice onDismiss={ onDismiss } /> )
190+
details.insertAdjacentElement( 'afterend', noticeDiv )
191+
}
192+
193+
return result
194+
},
195+
} )
196+
197+
wp.media.view.Attachment.Details = CustomDetailsView
198+
} )
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.stk-cimo-notice {
2+
clear: both;
3+
padding: 16px;
4+
border: 2px solid #16a249;
5+
background: #fff;
6+
margin: 12px 0;
7+
border-radius: 4px;
8+
box-shadow: 0 1px 4px #33533f70;
9+
position: relative;
10+
11+
button {
12+
background: none;
13+
border: none;
14+
height: 14px;
15+
width: 14px;
16+
position: absolute;
17+
right: 4px;
18+
top: 4px;
19+
cursor: pointer;
20+
}
21+
22+
.dashicon {
23+
font-size: 14px;
24+
height: 14px;
25+
width: 14px;
26+
&:hover {
27+
color: var(--stk-skin-error, #f15449);
28+
}
29+
}
30+
31+
p {
32+
margin: 0;
33+
font-size: 12px;
34+
35+
a {
36+
color: inherit;
37+
font-weight: 700;
38+
39+
&:hover {
40+
color: #16a249;
41+
}
42+
}
43+
span {
44+
font-weight: 700;
45+
}
46+
}
47+
}

src/welcome/admin.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { GettingStarted } from './getting-started'
4242
import { BLOCK_STATE } from '~stackable/util/blocks'
4343
import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin'
4444
import blockData from '~stackable/deprecated/v2/welcome/blocks'
45+
import { UsefulPlugins } from './useful-plugins'
4546

4647
const [ FREE_BLOCKS, BLOCK_DEPENDENCIES ] = importBlocks( require.context( '../block', true, /block\.json$/ ) )
4748

@@ -1658,4 +1659,12 @@ domReady( () => {
16581659
<Settings />
16591660
)
16601661
}
1662+
1663+
if ( document.getElementById( 's-useful-plugins' ) ) {
1664+
createRoot(
1665+
document.getElementById( 's-useful-plugins' )
1666+
).render(
1667+
<UsefulPlugins />
1668+
)
1669+
}
16611670
} )

0 commit comments

Comments
 (0)