From 26f23def504f86ca435bde776a3dd51c53c17c76 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 24 Oct 2019 10:13:05 -0700 Subject: [PATCH] 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`. --- .../activity-panel/panels/inbox/action.js | 79 +++++++++++++ .../activity-panel/panels/inbox/card.js | 104 ++++++++++++++++++ .../panels/{inbox.js => inbox/index.js} | 62 +---------- plugins/woocommerce-admin/package-lock.json | 8 ++ plugins/woocommerce-admin/package.json | 1 + .../woocommerce-admin/src/Notes/DataStore.php | 28 ++++- .../src/Notes/WC_Admin_Note.php | 17 +++ .../WC_Admin_Notes_Facebook_Extension.php | 21 +++- 8 files changed, 261 insertions(+), 59 deletions(-) create mode 100644 plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/action.js create mode 100644 plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/card.js rename plugins/woocommerce-admin/client/header/activity-panel/panels/{inbox.js => inbox/index.js} (65%) diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/action.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/action.js new file mode 100644 index 00000000000..1880b8ed420 --- /dev/null +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/action.js @@ -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 ( + + ); + } +} + +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 ); diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/card.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/card.js new file mode 100644 index 00000000000..8432463514a --- /dev/null +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/card.js @@ -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 => ); + }; + + return ( + + } + unread={ + ! lastRead || + ! note.date_created_gmt || + new Date( note.date_created_gmt + 'Z' ).getTime() > lastRead + } + actions={ getButtonsFromActions( note ) } + > + + + + ); + } +} + +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; diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/index.js similarity index 65% rename from plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js rename to plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/index.js index d522678e145..add0937846f 100644 --- a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox/index.js @@ -3,27 +3,20 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; import { Component, Fragment } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import Gridicon from 'gridicons'; import { withDispatch } from '@wordpress/data'; -/** - * WooCommerce dependencies - */ -import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings'; - /** * Internal dependencies */ -import { ActivityCard, ActivityCardPlaceholder } from '../activity-card'; -import ActivityHeader from '../activity-header'; +import { ActivityCard, ActivityCardPlaceholder } from '../../activity-card'; +import ActivityHeader from '../../activity-header'; +import InboxNoteCard from './card'; import { EmptyContent, Section } from '@woocommerce/components'; -import sanitizeHTML from 'lib/sanitize-html'; import { QUERY_DEFAULTS } from 'wc-api/constants'; import withSelect from 'wc-api/with-select'; -import classnames from 'classnames'; class InboxPanel extends Component { constructor( props ) { @@ -38,18 +31,6 @@ class InboxPanel extends Component { 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() { return ( { - if ( ! note.actions ) { - return []; - } - return note.actions.map( action => ( - - ) ); - }; - const notesArray = Object.keys( notes ).map( key => notes[ key ] ); return notesArray.map( note => ( - } - unread={ - ! lastRead || - ! note.date_created_gmt || - new Date( note.date_created_gmt + 'Z' ).getTime() > lastRead - } - actions={ getButtonsFromActions( note ) } - > - - + ) ); } @@ -180,11 +129,10 @@ export default compose( return { notes, isError, isRequesting, lastRead: userData.activity_panel_inbox_last_read }; } ), withDispatch( dispatch => { - const { updateCurrentUserData, triggerNoteAction } = dispatch( 'wc-api' ); + const { updateCurrentUserData } = dispatch( 'wc-api' ); return { updateCurrentUserData, - triggerNoteAction, }; } ) )( InboxPanel ); diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 959c50bb38c..63ca38423ec 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -16775,6 +16775,14 @@ "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": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.1.tgz", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 1e3ac156255..7b125d7f426 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -106,6 +106,7 @@ "react-dates": "17.2.0", "react-router-dom": "5.1.2", "react-transition-group": "2.9.0", + "react-visibility-sensor": "5.1.1", "redux": "4.0.4" }, "devDependencies": { diff --git a/plugins/woocommerce-admin/src/Notes/DataStore.php b/plugins/woocommerce-admin/src/Notes/DataStore.php index 6524da135b6..94a318fa352 100644 --- a/plugins/woocommerce-admin/src/Notes/DataStore.php +++ b/plugins/woocommerce-admin/src/Notes/DataStore.php @@ -244,7 +244,30 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter 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( 'note_id' => $note->get_id(), 'name' => $action->name, @@ -274,6 +297,9 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter $data_format ); } + + // Update actions from DB (to grab new IDs). + $this->read_actions( $note ); } /** diff --git a/plugins/woocommerce-admin/src/Notes/WC_Admin_Note.php b/plugins/woocommerce-admin/src/Notes/WC_Admin_Note.php index 5e520d50abd..c7c31fd46e7 100644 --- a/plugins/woocommerce-admin/src/Notes/WC_Admin_Note.php +++ b/plugins/woocommerce-admin/src/Notes/WC_Admin_Note.php @@ -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 diff --git a/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Facebook_Extension.php b/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Facebook_Extension.php index 28562fadd52..acce69ea808 100644 --- a/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Facebook_Extension.php +++ b/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Facebook_Extension.php @@ -69,7 +69,7 @@ class WC_Admin_Notes_Facebook_Extension { $note->set_name( self::NOTE_NAME ); $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( '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. 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' ); $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 + ); } } }