Add Inbox note action indication (https://github.com/woocommerce/woocommerce-admin/pull/3039)
* Move inbox note actions to a bespoke component. * Set busy state on action buttons on click. * Allow for Note actions to be deleted. * Update FB extension note after installation is complete. * Link actions don't get busy treatment. * Re-fetch note actions after updating. Get new action IDs from the database. * Add tracking to inbox note views. (https://github.com/woocommerce/woocommerce-admin/pull/3096) * Move inbox note content to its own component. * Send a tracks event when inbox notes are in the viewport. Uses react-visibility-sensor. * Match event data to `inbox_action_click`.
This commit is contained in:
parent
1ac8577fc2
commit
26f23def50
|
@ -0,0 +1,79 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
|
import { withDispatch } from '@wordpress/data';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InboxNoteAction extends Component {
|
||||||
|
constructor( props ) {
|
||||||
|
super( props );
|
||||||
|
this.state = {
|
||||||
|
inAction: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleActionClick = this.handleActionClick.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActionClick( event ) {
|
||||||
|
const { action, noteId, triggerNoteAction } = this.props;
|
||||||
|
const href = event.target.href || '';
|
||||||
|
let inAction = true;
|
||||||
|
|
||||||
|
if ( href.length && ! href.startsWith( adminUrl ) ) {
|
||||||
|
event.preventDefault();
|
||||||
|
inAction = false; // link buttons shouldn't be "busy".
|
||||||
|
window.open( href, '_blank' );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState( { inAction }, () => triggerNoteAction( noteId, action.id ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { action } = this.props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isDefault
|
||||||
|
isPrimary={ action.primary }
|
||||||
|
isBusy={ this.state.inAction }
|
||||||
|
disabled={ this.state.inAction }
|
||||||
|
href={ action.url || undefined }
|
||||||
|
onClick={ this.handleActionClick }
|
||||||
|
>
|
||||||
|
{ action.label }
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InboxNoteAction.propTypes = {
|
||||||
|
noteId: PropTypes.number,
|
||||||
|
action: PropTypes.shape( {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
primary: PropTypes.bool.isRequired,
|
||||||
|
} ),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withDispatch( dispatch => {
|
||||||
|
const { triggerNoteAction } = dispatch( 'wc-api' );
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggerNoteAction,
|
||||||
|
};
|
||||||
|
} )
|
||||||
|
)( InboxNoteAction );
|
|
@ -0,0 +1,104 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Gridicon from 'gridicons';
|
||||||
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ActivityCard } from '../../activity-card';
|
||||||
|
import NoteAction from './action';
|
||||||
|
import sanitizeHTML from 'lib/sanitize-html';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { recordEvent } from 'lib/tracks';
|
||||||
|
|
||||||
|
class InboxNoteCard extends Component {
|
||||||
|
constructor( props ) {
|
||||||
|
super( props );
|
||||||
|
this.onVisible = this.onVisible.bind( this );
|
||||||
|
this.hasBeenSeen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a view Tracks event when the note is seen.
|
||||||
|
onVisible( isVisible ) {
|
||||||
|
if ( isVisible && ! this.hasBeenSeen ) {
|
||||||
|
const { note } = this.props;
|
||||||
|
const {
|
||||||
|
content: note_content,
|
||||||
|
name: note_name,
|
||||||
|
title: note_title,
|
||||||
|
type: note_type,
|
||||||
|
icon: note_icon,
|
||||||
|
} = note;
|
||||||
|
|
||||||
|
recordEvent( 'inbox_note_view', {
|
||||||
|
note_content,
|
||||||
|
note_name,
|
||||||
|
note_title,
|
||||||
|
note_type,
|
||||||
|
note_icon,
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.hasBeenSeen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { lastRead, note } = this.props;
|
||||||
|
|
||||||
|
const getButtonsFromActions = () => {
|
||||||
|
if ( ! note.actions ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return note.actions.map( action => <NoteAction noteId={ note.id } action={ action } /> );
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisibilitySensor onChange={ this.onVisible }>
|
||||||
|
<ActivityCard
|
||||||
|
className={ classnames( 'woocommerce-inbox-activity-card', {
|
||||||
|
actioned: 'unactioned' !== note.status,
|
||||||
|
} ) }
|
||||||
|
title={ note.title }
|
||||||
|
date={ note.date_created }
|
||||||
|
icon={ <Gridicon icon={ note.icon } size={ 48 } /> }
|
||||||
|
unread={
|
||||||
|
! lastRead ||
|
||||||
|
! note.date_created_gmt ||
|
||||||
|
new Date( note.date_created_gmt + 'Z' ).getTime() > lastRead
|
||||||
|
}
|
||||||
|
actions={ getButtonsFromActions( note ) }
|
||||||
|
>
|
||||||
|
<span dangerouslySetInnerHTML={ sanitizeHTML( note.content ) } />
|
||||||
|
</ActivityCard>
|
||||||
|
</VisibilitySensor>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InboxNoteCard.propTypes = {
|
||||||
|
note: PropTypes.shape( {
|
||||||
|
id: PropTypes.number,
|
||||||
|
status: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
date_created: PropTypes.string,
|
||||||
|
date_created_gmt: PropTypes.string,
|
||||||
|
actions: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape( {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
primary: PropTypes.bool.isRequired,
|
||||||
|
} )
|
||||||
|
),
|
||||||
|
} ),
|
||||||
|
lastRead: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InboxNoteCard;
|
|
@ -3,27 +3,20 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Button } from '@wordpress/components';
|
|
||||||
import { Component, Fragment } from '@wordpress/element';
|
import { Component, Fragment } from '@wordpress/element';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import Gridicon from 'gridicons';
|
import Gridicon from 'gridicons';
|
||||||
import { withDispatch } from '@wordpress/data';
|
import { withDispatch } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
|
||||||
* WooCommerce dependencies
|
|
||||||
*/
|
|
||||||
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
|
import { ActivityCard, ActivityCardPlaceholder } from '../../activity-card';
|
||||||
import ActivityHeader from '../activity-header';
|
import ActivityHeader from '../../activity-header';
|
||||||
|
import InboxNoteCard from './card';
|
||||||
import { EmptyContent, Section } from '@woocommerce/components';
|
import { EmptyContent, Section } from '@woocommerce/components';
|
||||||
import sanitizeHTML from 'lib/sanitize-html';
|
|
||||||
import { QUERY_DEFAULTS } from 'wc-api/constants';
|
import { QUERY_DEFAULTS } from 'wc-api/constants';
|
||||||
import withSelect from 'wc-api/with-select';
|
import withSelect from 'wc-api/with-select';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
class InboxPanel extends Component {
|
class InboxPanel extends Component {
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
|
@ -38,18 +31,6 @@ class InboxPanel extends Component {
|
||||||
this.props.updateCurrentUserData( userDataFields );
|
this.props.updateCurrentUserData( userDataFields );
|
||||||
}
|
}
|
||||||
|
|
||||||
handleActionClick( event, note_id, action_id ) {
|
|
||||||
const { triggerNoteAction } = this.props;
|
|
||||||
const href = event.target.href || '';
|
|
||||||
|
|
||||||
if ( href.length && ! href.startsWith( adminUrl ) ) {
|
|
||||||
event.preventDefault();
|
|
||||||
window.open( href, '_blank' );
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerNoteAction( note_id, action_id );
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEmptyCard() {
|
renderEmptyCard() {
|
||||||
return (
|
return (
|
||||||
<ActivityCard
|
<ActivityCard
|
||||||
|
@ -73,42 +54,10 @@ class InboxPanel extends Component {
|
||||||
return this.renderEmptyCard();
|
return this.renderEmptyCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
const getButtonsFromActions = note => {
|
|
||||||
if ( ! note.actions ) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return note.actions.map( action => (
|
|
||||||
<Button
|
|
||||||
isDefault
|
|
||||||
isPrimary={ action.primary }
|
|
||||||
href={ action.url || undefined }
|
|
||||||
onClick={ e => this.handleActionClick( e, note.id, action.id ) }
|
|
||||||
>
|
|
||||||
{ action.label }
|
|
||||||
</Button>
|
|
||||||
) );
|
|
||||||
};
|
|
||||||
|
|
||||||
const notesArray = Object.keys( notes ).map( key => notes[ key ] );
|
const notesArray = Object.keys( notes ).map( key => notes[ key ] );
|
||||||
|
|
||||||
return notesArray.map( note => (
|
return notesArray.map( note => (
|
||||||
<ActivityCard
|
<InboxNoteCard key={ note.id } note={ note } lastRead={ lastRead } />
|
||||||
key={ note.id }
|
|
||||||
className={ classnames( 'woocommerce-inbox-activity-card', {
|
|
||||||
actioned: 'unactioned' !== note.status,
|
|
||||||
} ) }
|
|
||||||
title={ note.title }
|
|
||||||
date={ note.date_created }
|
|
||||||
icon={ <Gridicon icon={ note.icon } size={ 48 } /> }
|
|
||||||
unread={
|
|
||||||
! lastRead ||
|
|
||||||
! note.date_created_gmt ||
|
|
||||||
new Date( note.date_created_gmt + 'Z' ).getTime() > lastRead
|
|
||||||
}
|
|
||||||
actions={ getButtonsFromActions( note ) }
|
|
||||||
>
|
|
||||||
<span dangerouslySetInnerHTML={ sanitizeHTML( note.content ) } />
|
|
||||||
</ActivityCard>
|
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,11 +129,10 @@ export default compose(
|
||||||
return { notes, isError, isRequesting, lastRead: userData.activity_panel_inbox_last_read };
|
return { notes, isError, isRequesting, lastRead: userData.activity_panel_inbox_last_read };
|
||||||
} ),
|
} ),
|
||||||
withDispatch( dispatch => {
|
withDispatch( dispatch => {
|
||||||
const { updateCurrentUserData, triggerNoteAction } = dispatch( 'wc-api' );
|
const { updateCurrentUserData } = dispatch( 'wc-api' );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateCurrentUserData,
|
updateCurrentUserData,
|
||||||
triggerNoteAction,
|
|
||||||
};
|
};
|
||||||
} )
|
} )
|
||||||
)( InboxPanel );
|
)( InboxPanel );
|
|
@ -16775,6 +16775,14 @@
|
||||||
"react-lifecycles-compat": "^3.0.4"
|
"react-lifecycles-compat": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-visibility-sensor": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-with-direction": {
|
"react-with-direction": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.1.tgz",
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"react-dates": "17.2.0",
|
"react-dates": "17.2.0",
|
||||||
"react-router-dom": "5.1.2",
|
"react-router-dom": "5.1.2",
|
||||||
"react-transition-group": "2.9.0",
|
"react-transition-group": "2.9.0",
|
||||||
|
"react-visibility-sensor": "5.1.1",
|
||||||
"redux": "4.0.4"
|
"redux": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -244,7 +244,30 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $note->get_actions( 'edit' ) as $action ) {
|
// Process action removal. Actions are removed from
|
||||||
|
// the note if they aren't part of the changeset.
|
||||||
|
// See WC_Admin_Note::add_action().
|
||||||
|
$changed_actions = $note->get_actions( 'edit' );
|
||||||
|
$actions_to_keep = array();
|
||||||
|
|
||||||
|
foreach ( $changed_actions as $action ) {
|
||||||
|
if ( ! empty( $action->id ) ) {
|
||||||
|
$actions_to_keep[] = (int) $action->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clear_actions_query = $wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}wc_admin_note_actions WHERE note_id = %d", $note->get_id()
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $actions_to_keep ) {
|
||||||
|
$clear_actions_query .= sprintf( ' AND action_id NOT IN (%s)', implode( ',', $actions_to_keep ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->query( $clear_actions_query );
|
||||||
|
|
||||||
|
// Update/insert the actions in this changeset.
|
||||||
|
foreach ( $changed_actions as $action ) {
|
||||||
$action_data = array(
|
$action_data = array(
|
||||||
'note_id' => $note->get_id(),
|
'note_id' => $note->get_id(),
|
||||||
'name' => $action->name,
|
'name' => $action->name,
|
||||||
|
@ -274,6 +297,9 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter
|
||||||
$data_format
|
$data_format
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update actions from DB (to grab new IDs).
|
||||||
|
$this->read_actions( $note );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -84,6 +84,23 @@ class WC_Admin_Note extends \WC_Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge changes with data and clear.
|
||||||
|
*
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
public function apply_changes() {
|
||||||
|
$this->data = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine
|
||||||
|
|
||||||
|
// Note actions need to be replaced wholesale.
|
||||||
|
// Merging arrays doesn't allow for deleting note actions.
|
||||||
|
if ( isset( $this->changes['actions'] ) ) {
|
||||||
|
$this->data['actions'] = $this->changes['actions'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->changes = array();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Helpers
|
| Helpers
|
||||||
|
|
|
@ -69,7 +69,7 @@ class WC_Admin_Notes_Facebook_Extension {
|
||||||
$note->set_name( self::NOTE_NAME );
|
$note->set_name( self::NOTE_NAME );
|
||||||
$note->set_source( 'woocommerce-admin' );
|
$note->set_source( 'woocommerce-admin' );
|
||||||
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce-admin' ), 'https://woocommerce.com/products/facebook/', WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED );
|
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce-admin' ), 'https://woocommerce.com/products/facebook/', WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED );
|
||||||
$note->add_action( 'install-now', __( 'Install now', 'woocommerce-admin' ), false, WC_Admin_Note::E_WC_ADMIN_NOTE_ACTIONED, true );
|
$note->add_action( 'install-now', __( 'Install now', 'woocommerce-admin' ), false, WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED, true );
|
||||||
|
|
||||||
// Create the note as "actioned" if the Facebook extension is already installed.
|
// Create the note as "actioned" if the Facebook extension is already installed.
|
||||||
if ( 0 === validate_plugin( 'facebook-for-woocommerce/facebook-for-woocommerce.php' ) ) {
|
if ( 0 === validate_plugin( 'facebook-for-woocommerce/facebook-for-woocommerce.php' ) ) {
|
||||||
|
@ -97,6 +97,25 @@ class WC_Admin_Notes_Facebook_Extension {
|
||||||
|
|
||||||
$activate_request = array( 'plugins' => 'facebook-for-woocommerce' );
|
$activate_request = array( 'plugins' => 'facebook-for-woocommerce' );
|
||||||
$installer->activate_plugins( $activate_request );
|
$installer->activate_plugins( $activate_request );
|
||||||
|
|
||||||
|
$content = __( 'You\'re almost ready to start driving sales with Facebook. Complete the setup steps to control how WooCommerce integrates with your Facebook store.', 'woocommerce-admin' );
|
||||||
|
$note->set_title( __( 'Market on Facebook — Installed', 'woocommerce-admin' ) );
|
||||||
|
$note->set_content( $content );
|
||||||
|
$note->set_icon( 'checkmark-circle' );
|
||||||
|
$note->clear_actions();
|
||||||
|
$note->add_action(
|
||||||
|
'configure-facebook',
|
||||||
|
__( 'Setup', 'woocommerce-admin' ),
|
||||||
|
add_query_arg(
|
||||||
|
array(
|
||||||
|
'page' => 'wc-settings',
|
||||||
|
'tab' => 'integration',
|
||||||
|
'section' => 'facebookcommerce',
|
||||||
|
),
|
||||||
|
admin_url( 'admin.php' )
|
||||||
|
),
|
||||||
|
WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue