* 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:
Jeff Stieler 2019-10-24 10:13:05 -07:00 committed by GitHub
parent 1ac8577fc2
commit 26f23def50
8 changed files with 261 additions and 59 deletions

View File

@ -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 );

View File

@ -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;

View File

@ -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 );

View File

@ -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",

View File

@ -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": {

View File

@ -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 );
} }
/** /**

View File

@ -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

View File

@ -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
);
} }
} }
} }