Refactor and extract inbox panel components (https://github.com/woocommerce/woocommerce-admin/pull/7006)
* Refactored the inbox note cards and moved to experimental package * Update experimental dependencies * Add tests for inbox-note components, and updated naming * Add changelog * Update readme and fix dismiss all * Fixed lint errors * Refactor dismiss logic in inbox-panel * Add hook for handling inner link callbacks * Export updates and a minor TS update * Fix lint error
This commit is contained in:
parent
1b022ae4de
commit
65de4bff00
|
@ -1,184 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
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';
|
||||
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
|
||||
import { NOTES_STORE_NAME } from '@woocommerce/data';
|
||||
|
||||
class InboxNoteAction extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
inAction: false,
|
||||
};
|
||||
|
||||
this.handleActionClick = this.handleActionClick.bind( this );
|
||||
}
|
||||
|
||||
handleActionClick( event ) {
|
||||
const {
|
||||
action,
|
||||
actionCallback,
|
||||
batchUpdateNotes,
|
||||
createNotice,
|
||||
noteId,
|
||||
triggerNoteAction,
|
||||
removeAllNotes,
|
||||
removeNote,
|
||||
onClick,
|
||||
updateNote,
|
||||
} = 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' );
|
||||
}
|
||||
|
||||
if ( ! action ) {
|
||||
if ( noteId ) {
|
||||
removeNote( noteId )
|
||||
.then( () => {
|
||||
createNotice(
|
||||
'success',
|
||||
__( 'Message dismissed', 'woocommerce-admin' ),
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __(
|
||||
'Undo',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
onClick: () => {
|
||||
updateNote( noteId, {
|
||||
is_deleted: 0,
|
||||
} );
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
} )
|
||||
.catch( () => {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'Message could not be dismissed',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
} );
|
||||
} else {
|
||||
removeAllNotes()
|
||||
.then( ( notes ) => {
|
||||
createNotice(
|
||||
'success',
|
||||
__( 'All messages dismissed', 'woocommerce-admin' ),
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __(
|
||||
'Undo',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
onClick: () => {
|
||||
batchUpdateNotes(
|
||||
notes.map(
|
||||
( note ) => note.id
|
||||
),
|
||||
{
|
||||
is_deleted: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
} )
|
||||
.catch( () => {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'Message could not be dismissed',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
actionCallback( true );
|
||||
} else {
|
||||
this.setState( { inAction }, () => {
|
||||
triggerNoteAction( noteId, action.id );
|
||||
|
||||
if ( !! onClick ) {
|
||||
onClick();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { action, dismiss, label } = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
isSecondary
|
||||
isBusy={ this.state.inAction }
|
||||
disabled={ this.state.inAction }
|
||||
href={
|
||||
action && action.url && action.url.length
|
||||
? action.url
|
||||
: undefined
|
||||
}
|
||||
onClick={ this.handleActionClick }
|
||||
>
|
||||
{ dismiss ? label : action.label }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InboxNoteAction.propTypes = {
|
||||
noteId: PropTypes.number,
|
||||
label: PropTypes.string,
|
||||
dismiss: PropTypes.bool,
|
||||
actionCallback: PropTypes.func,
|
||||
action: PropTypes.shape( {
|
||||
id: PropTypes.number.isRequired,
|
||||
url: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
primary: PropTypes.bool.isRequired,
|
||||
} ),
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
const {
|
||||
batchUpdateNotes,
|
||||
removeAllNotes,
|
||||
removeNote,
|
||||
updateNote,
|
||||
triggerNoteAction,
|
||||
} = dispatch( NOTES_STORE_NAME );
|
||||
|
||||
return {
|
||||
batchUpdateNotes,
|
||||
createNotice,
|
||||
removeAllNotes,
|
||||
removeNote,
|
||||
triggerNoteAction,
|
||||
updateNote,
|
||||
};
|
||||
} )
|
||||
)( InboxNoteAction );
|
|
@ -1,371 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import { Button, Dropdown, Modal } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import moment from 'moment';
|
||||
import classnames from 'classnames';
|
||||
import { H, Section } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import NoteAction from './action';
|
||||
import sanitizeHTML from '../lib/sanitize-html';
|
||||
import './style.scss';
|
||||
import { getScreenName } from '../utils';
|
||||
|
||||
class InboxNoteCard extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.onVisible = this.onVisible.bind( this );
|
||||
this.hasBeenSeen = false;
|
||||
this.state = {
|
||||
isDismissModalOpen: false,
|
||||
dismissType: null,
|
||||
clickedActionText: null,
|
||||
};
|
||||
this.openDismissModal = this.openDismissModal.bind( this );
|
||||
this.closeDismissModal = this.closeDismissModal.bind( this );
|
||||
this.bodyNotificationRef = createRef();
|
||||
this.toggleButtonRef = createRef();
|
||||
this.screen = getScreenName();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if ( this.bodyNotificationRef.current ) {
|
||||
this.bodyNotificationRef.current.addEventListener(
|
||||
'click',
|
||||
( event ) => this.handleBodyClick( event, this.props )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if ( this.bodyNotificationRef.current ) {
|
||||
this.bodyNotificationRef.current.removeEventListener(
|
||||
'click',
|
||||
( event ) => this.handleBodyClick( event, this.props )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleBodyClick( event, props ) {
|
||||
const innerLink = event.target.href;
|
||||
if ( innerLink ) {
|
||||
const { note } = props;
|
||||
|
||||
recordEvent( 'wcadmin_inbox_action_click', {
|
||||
note_name: note.name,
|
||||
note_title: note.title,
|
||||
note_content_inner_link: innerLink,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a view Tracks event when the note is seen.
|
||||
onVisible( isVisible ) {
|
||||
if ( isVisible && ! this.hasBeenSeen ) {
|
||||
const { note } = this.props;
|
||||
|
||||
recordEvent( 'inbox_note_view', {
|
||||
note_content: note.content,
|
||||
note_name: note.name,
|
||||
note_title: note.title,
|
||||
note_type: note.type,
|
||||
screen: this.screen,
|
||||
} );
|
||||
|
||||
this.hasBeenSeen = true;
|
||||
}
|
||||
}
|
||||
|
||||
openDismissModal( type, onToggle ) {
|
||||
this.setState( {
|
||||
isDismissModalOpen: true,
|
||||
dismissType: type,
|
||||
} );
|
||||
onToggle();
|
||||
}
|
||||
|
||||
closeDismissModal( noteNameDismissConfirmation ) {
|
||||
const { dismissType } = this.state;
|
||||
const { note } = this.props;
|
||||
const noteNameDismissAll = dismissType === 'all' ? true : false;
|
||||
|
||||
recordEvent( 'inbox_action_dismiss', {
|
||||
note_name: note.name,
|
||||
note_title: note.title,
|
||||
note_name_dismiss_all: noteNameDismissAll,
|
||||
note_name_dismiss_confirmation:
|
||||
noteNameDismissConfirmation || false,
|
||||
screen: this.screen,
|
||||
} );
|
||||
|
||||
this.setState( {
|
||||
isDismissModalOpen: false,
|
||||
} );
|
||||
}
|
||||
|
||||
handleBlur( event, onClose ) {
|
||||
const dropdownClasses = [
|
||||
'woocommerce-admin-dismiss-notification',
|
||||
'components-popover__content',
|
||||
];
|
||||
// This line is for IE compatibility.
|
||||
let relatedTarget;
|
||||
if ( event.relatedTarget ) {
|
||||
relatedTarget = event.relatedTarget;
|
||||
} else if ( this.toggleButtonRef.current ) {
|
||||
const ownerDoc = this.toggleButtonRef.current.ownerDocument;
|
||||
relatedTarget = ownerDoc ? ownerDoc.activeElement : null;
|
||||
}
|
||||
const isClickOutsideDropdown = relatedTarget
|
||||
? dropdownClasses.some( ( className ) =>
|
||||
relatedTarget.className.includes( className )
|
||||
)
|
||||
: false;
|
||||
if ( isClickOutsideDropdown ) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
renderDismissButton() {
|
||||
const { clickedActionText } = this.state;
|
||||
|
||||
if ( clickedActionText ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
contentClassName="woocommerce-admin-dismiss-dropdown"
|
||||
position="bottom right"
|
||||
renderToggle={ ( { onClose, onToggle } ) => (
|
||||
<Button
|
||||
isTertiary
|
||||
onClick={ onToggle }
|
||||
ref={ this.toggleButtonRef }
|
||||
onBlur={ ( event ) =>
|
||||
this.handleBlur( event, onClose )
|
||||
}
|
||||
>
|
||||
{ __( 'Dismiss', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
focusOnMount={ false }
|
||||
popoverProps={ { noArrow: true } }
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<ul>
|
||||
<li>
|
||||
<Button
|
||||
className="woocommerce-admin-dismiss-notification"
|
||||
onClick={ () =>
|
||||
this.openDismissModal( 'this', onToggle )
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Dismiss this message',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
className="woocommerce-admin-dismiss-notification"
|
||||
onClick={ () =>
|
||||
this.openDismissModal( 'all', onToggle )
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Dismiss all messages',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getDismissConfirmationButton() {
|
||||
const { note } = this.props;
|
||||
const { dismissType } = this.state;
|
||||
return (
|
||||
<NoteAction
|
||||
key={ note.id }
|
||||
noteId={ dismissType === 'all' ? null : note.id }
|
||||
label={ __( "Yes, I'm sure", 'woocommerce-admin' ) }
|
||||
actionCallback={ this.closeDismissModal }
|
||||
dismiss={ true }
|
||||
screen={ this.screen }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDismissConfirmationModal() {
|
||||
return (
|
||||
<Modal
|
||||
title={ <>{ __( 'Are you sure?', 'woocommerce-admin' ) }</> }
|
||||
onRequestClose={ () => this.closeDismissModal() }
|
||||
className="woocommerce-inbox-dismiss-confirmation_modal"
|
||||
>
|
||||
<div className="woocommerce-inbox-dismiss-confirmation_wrapper">
|
||||
<p>
|
||||
{ __(
|
||||
'Dismissed messages cannot be viewed again',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
<div className="woocommerce-inbox-dismiss-confirmation_buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={ () => this.closeDismissModal() }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
{ this.getDismissConfirmationButton() }
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderActions( note ) {
|
||||
const { actions: noteActions, id: noteId } = note;
|
||||
const { clickedActionText } = this.state;
|
||||
|
||||
if ( !! clickedActionText ) {
|
||||
return clickedActionText;
|
||||
}
|
||||
|
||||
if ( ! noteActions ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ noteActions.map( ( action, index ) => (
|
||||
<NoteAction
|
||||
key={ index }
|
||||
noteId={ noteId }
|
||||
action={ action }
|
||||
onClick={ () => this.onActionClicked( action ) }
|
||||
/>
|
||||
) ) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
onActionClicked = ( action ) => {
|
||||
if ( ! action.actioned_text ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState( {
|
||||
clickedActionText: action.actioned_text,
|
||||
} );
|
||||
};
|
||||
|
||||
render() {
|
||||
const { lastRead, note } = this.props;
|
||||
const { isDismissModalOpen } = this.state;
|
||||
const {
|
||||
content,
|
||||
date_created: dateCreated,
|
||||
date_created_gmt: dateCreatedGmt,
|
||||
image,
|
||||
is_deleted: isDeleted,
|
||||
layout,
|
||||
status,
|
||||
title,
|
||||
} = note;
|
||||
|
||||
if ( isDeleted ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unread =
|
||||
! lastRead ||
|
||||
! dateCreatedGmt ||
|
||||
new Date( dateCreatedGmt + 'Z' ).getTime() > lastRead;
|
||||
const date = dateCreated;
|
||||
const hasImage = layout !== 'plain' && layout !== '';
|
||||
const cardClassName = classnames( 'woocommerce-inbox-message', layout, {
|
||||
'message-is-unread': unread && status === 'unactioned',
|
||||
} );
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={ this.onVisible }>
|
||||
<section className={ cardClassName }>
|
||||
{ hasImage && (
|
||||
<div className="woocommerce-inbox-message__image">
|
||||
<img src={ image } alt="" />
|
||||
</div>
|
||||
) }
|
||||
<div className="woocommerce-inbox-message__wrapper">
|
||||
<div className="woocommerce-inbox-message__content">
|
||||
{ unread && (
|
||||
<div className="woocommerce-inbox-message__unread-indicator" />
|
||||
) }
|
||||
{ date && (
|
||||
<span className="woocommerce-inbox-message__date">
|
||||
{ moment.utc( date ).fromNow() }
|
||||
</span>
|
||||
) }
|
||||
<H className="woocommerce-inbox-message__title">
|
||||
{ title }
|
||||
</H>
|
||||
<Section className="woocommerce-inbox-message__text">
|
||||
<span
|
||||
dangerouslySetInnerHTML={ sanitizeHTML(
|
||||
content
|
||||
) }
|
||||
ref={ this.bodyNotificationRef }
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__actions">
|
||||
{ this.renderActions( note ) }
|
||||
{ this.renderDismissButton() }
|
||||
</div>
|
||||
</div>
|
||||
{ isDismissModalOpen &&
|
||||
this.renderDismissConfirmationModal() }
|
||||
</section>
|
||||
</VisibilitySensor>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InboxNoteCard.propTypes = {
|
||||
note: PropTypes.shape( {
|
||||
id: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
title: 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,
|
||||
} )
|
||||
),
|
||||
layout: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
is_deleted: PropTypes.bool,
|
||||
} ),
|
||||
lastRead: PropTypes.number,
|
||||
};
|
||||
|
||||
export default InboxNoteCard;
|
|
@ -1,25 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, _n } from '@wordpress/i18n';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { EmptyContent, Section } from '@woocommerce/components';
|
||||
import {
|
||||
NOTES_STORE_NAME,
|
||||
useUserPreferences,
|
||||
QUERY_DEFAULTS,
|
||||
} from '@woocommerce/data';
|
||||
import { withSelect } from '@wordpress/data';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import {
|
||||
InboxNoteCard,
|
||||
InboxDismissConfirmationModal,
|
||||
InboxNotePlaceholder,
|
||||
} from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ActivityCard } from '../header/activity-panel/activity-card';
|
||||
import InboxNotePlaceholder from './placeholder';
|
||||
import InboxNoteCard from './card';
|
||||
import { hasValidNotes } from './utils';
|
||||
import { getScreenName } from '../utils';
|
||||
import './index.scss';
|
||||
|
||||
const renderEmptyCard = () => (
|
||||
<ActivityCard
|
||||
|
@ -35,7 +40,22 @@ const renderEmptyCard = () => (
|
|||
</ActivityCard>
|
||||
);
|
||||
|
||||
const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
|
||||
const onBodyLinkClick = ( note, innerLink ) => {
|
||||
recordEvent( 'inbox_action_click', {
|
||||
note_name: note.name,
|
||||
note_title: note.title,
|
||||
note_content_inner_link: innerLink,
|
||||
} );
|
||||
};
|
||||
|
||||
const renderNotes = ( {
|
||||
hasNotes,
|
||||
isBatchUpdating,
|
||||
lastRead,
|
||||
notes,
|
||||
onDismiss,
|
||||
onNoteActionClick,
|
||||
} ) => {
|
||||
if ( isBatchUpdating ) {
|
||||
return;
|
||||
}
|
||||
|
@ -44,6 +64,17 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
|
|||
return renderEmptyCard();
|
||||
}
|
||||
|
||||
const screen = getScreenName();
|
||||
const onNoteVisible = ( note ) => {
|
||||
recordEvent( 'inbox_note_view', {
|
||||
note_content: note.content,
|
||||
note_name: note.name,
|
||||
note_title: note.title,
|
||||
note_type: note.type,
|
||||
screen,
|
||||
} );
|
||||
};
|
||||
|
||||
const notesArray = Object.keys( notes ).map( ( key ) => notes[ key ] );
|
||||
|
||||
return (
|
||||
|
@ -63,6 +94,10 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
|
|||
key={ noteId }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onDismiss={ onDismiss }
|
||||
onNoteActionClick={ onNoteActionClick }
|
||||
onBodyLinkClick={ onBodyLinkClick }
|
||||
onNoteVisible={ onNoteVisible }
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
|
@ -71,10 +106,60 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
const InboxPanel = ( props ) => {
|
||||
const { isError, isResolving, isBatchUpdating, notes } = props;
|
||||
const INBOX_QUERY = {
|
||||
page: 1,
|
||||
per_page: QUERY_DEFAULTS.pageSize,
|
||||
status: 'unactioned',
|
||||
type: QUERY_DEFAULTS.noteTypes,
|
||||
orderby: 'date',
|
||||
order: 'desc',
|
||||
_fields: [
|
||||
'id',
|
||||
'name',
|
||||
'title',
|
||||
'content',
|
||||
'type',
|
||||
'status',
|
||||
'actions',
|
||||
'date_created',
|
||||
'date_created_gmt',
|
||||
'layout',
|
||||
'image',
|
||||
'is_deleted',
|
||||
],
|
||||
};
|
||||
|
||||
const InboxPanel = () => {
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const {
|
||||
batchUpdateNotes,
|
||||
removeAllNotes,
|
||||
removeNote,
|
||||
updateNote,
|
||||
triggerNoteAction,
|
||||
} = useDispatch( NOTES_STORE_NAME );
|
||||
const { isError, isResolvingNotes, isBatchUpdating, notes } = useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getNotes,
|
||||
getNotesError,
|
||||
isResolving,
|
||||
isNotesRequesting,
|
||||
} = select( NOTES_STORE_NAME );
|
||||
|
||||
return {
|
||||
notes: getNotes( INBOX_QUERY ),
|
||||
isError: Boolean(
|
||||
getNotesError( 'getNotes', [ INBOX_QUERY ] )
|
||||
),
|
||||
isResolvingNotes: isResolving( 'getNotes', [ INBOX_QUERY ] ),
|
||||
isBatchUpdating: isNotesRequesting( 'batchUpdateNotes' ),
|
||||
};
|
||||
}
|
||||
);
|
||||
const { updateUserPreferences, ...userPrefs } = useUserPreferences();
|
||||
const [ lastRead ] = useState( userPrefs.activity_panel_inbox_last_read );
|
||||
const [ dismiss, setDismiss ] = useState();
|
||||
|
||||
useEffect( () => {
|
||||
const mountTime = Date.now();
|
||||
|
@ -85,6 +170,83 @@ const InboxPanel = ( props ) => {
|
|||
updateUserPreferences( userDataFields );
|
||||
}, [] );
|
||||
|
||||
const onDismiss = ( note, type ) => {
|
||||
setDismiss( { note, type } );
|
||||
};
|
||||
|
||||
const closeDismissModal = async ( confirmed = false ) => {
|
||||
const noteNameDismissAll = dismiss.type === 'all' ? true : false;
|
||||
const screen = getScreenName();
|
||||
|
||||
recordEvent( 'inbox_action_dismiss', {
|
||||
note_name: dismiss.note.name,
|
||||
note_title: dismiss.note.title,
|
||||
note_name_dismiss_all: noteNameDismissAll,
|
||||
note_name_dismiss_confirmation: confirmed,
|
||||
screen,
|
||||
} );
|
||||
|
||||
if ( confirmed ) {
|
||||
const noteId = dismiss.note.id;
|
||||
const removeAll = ! noteId || noteNameDismissAll;
|
||||
try {
|
||||
let notesRemoved = [];
|
||||
if ( removeAll ) {
|
||||
notesRemoved = await removeAllNotes();
|
||||
} else {
|
||||
const noteRemoved = await removeNote( noteId );
|
||||
notesRemoved = [ noteRemoved ];
|
||||
}
|
||||
setDismiss( undefined );
|
||||
createNotice(
|
||||
'success',
|
||||
notesRemoved.length > 1
|
||||
? __( 'All messages dismissed', 'woocommerce-admin' )
|
||||
: __( 'Message dismissed', 'woocommerce-admin' ),
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Undo', 'woocommerce-admin' ),
|
||||
onClick: () => {
|
||||
if ( notesRemoved.length > 1 ) {
|
||||
batchUpdateNotes(
|
||||
notesRemoved.map(
|
||||
( note ) => note.id
|
||||
),
|
||||
{
|
||||
is_deleted: 0,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
updateNote( noteId, {
|
||||
is_deleted: 0,
|
||||
} );
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
} catch ( e ) {
|
||||
const numberOfNotes = removeAll ? notes.length : 1;
|
||||
createNotice(
|
||||
'error',
|
||||
_n(
|
||||
'Message could not be dismissed',
|
||||
'Messages could not be dismissed',
|
||||
numberOfNotes,
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
setDismiss( undefined );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onNoteActionClick = ( note, action ) => {
|
||||
triggerNoteAction( note.id, action.id );
|
||||
};
|
||||
|
||||
if ( isError ) {
|
||||
const title = __(
|
||||
'There was an error getting your inbox. Please try again.',
|
||||
|
@ -113,63 +275,32 @@ const InboxPanel = ( props ) => {
|
|||
return (
|
||||
<>
|
||||
<div className="woocommerce-homepage-notes-wrapper">
|
||||
{ ( isResolving || isBatchUpdating ) && (
|
||||
{ ( isResolvingNotes || isBatchUpdating ) && (
|
||||
<Section>
|
||||
<InboxNotePlaceholder className="banner message-is-unread" />
|
||||
</Section>
|
||||
) }
|
||||
<Section>
|
||||
{ ! isResolving &&
|
||||
{ ! isResolvingNotes &&
|
||||
! isBatchUpdating &&
|
||||
renderNotes( {
|
||||
hasNotes,
|
||||
isBatchUpdating,
|
||||
lastRead,
|
||||
notes,
|
||||
onDismiss,
|
||||
onNoteActionClick,
|
||||
} ) }
|
||||
</Section>
|
||||
{ dismiss && (
|
||||
<InboxDismissConfirmationModal
|
||||
onClose={ closeDismissModal }
|
||||
onDismiss={ () => closeDismissModal( true ) }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const INBOX_QUERY = {
|
||||
page: 1,
|
||||
per_page: QUERY_DEFAULTS.pageSize,
|
||||
status: 'unactioned',
|
||||
type: QUERY_DEFAULTS.noteTypes,
|
||||
orderby: 'date',
|
||||
order: 'desc',
|
||||
_fields: [
|
||||
'id',
|
||||
'name',
|
||||
'title',
|
||||
'content',
|
||||
'type',
|
||||
'status',
|
||||
'actions',
|
||||
'date_created',
|
||||
'date_created_gmt',
|
||||
'layout',
|
||||
'image',
|
||||
'is_deleted',
|
||||
],
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSelect( ( select ) => {
|
||||
const {
|
||||
getNotes,
|
||||
getNotesError,
|
||||
isResolving,
|
||||
isNotesRequesting,
|
||||
} = select( NOTES_STORE_NAME );
|
||||
|
||||
return {
|
||||
notes: getNotes( INBOX_QUERY ),
|
||||
isError: Boolean( getNotesError( 'getNotes', [ INBOX_QUERY ] ) ),
|
||||
isResolving: isResolving( 'getNotes', [ INBOX_QUERY ] ),
|
||||
isBatchUpdating: isNotesRequesting( 'batchUpdateNotes' ),
|
||||
};
|
||||
} )
|
||||
)( InboxPanel );
|
||||
export default InboxPanel;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
.woocommerce-homepage-notes-wrapper {
|
||||
padding-top: $gap-large;
|
||||
}
|
||||
|
||||
#activity-panel-inbox {
|
||||
margin: 0 $gap-large;
|
||||
}
|
||||
.woocommerce-layout__inbox-panel-header {
|
||||
padding: $gap-large;
|
||||
|
||||
.woocommerce-homepage-column & {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-enter {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-enter-active {
|
||||
opacity: 1;
|
||||
max-height: 100vh;
|
||||
transform: translateX(0%);
|
||||
transition: opacity 500ms, transform 500ms, max-height 500ms;
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-exit {
|
||||
opacity: 1;
|
||||
max-height: 100vh;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-exit-active {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateX(50%);
|
||||
transition: opacity 500ms, transform 500ms, max-height 500ms;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class InboxNotePlaceholder extends Component {
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={ `woocommerce-inbox-message is-placeholder ${ className }` }
|
||||
aria-hidden
|
||||
>
|
||||
<div className="woocommerce-inbox-message__wrapper">
|
||||
<div className="woocommerce-inbox-message__content">
|
||||
<div className="woocommerce-inbox-message__date">
|
||||
<div className="sixth-line" />
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__title">
|
||||
<div className="line" />
|
||||
<div className="line" />
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__text">
|
||||
<div className="line" />
|
||||
<div className="third-line" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__actions">
|
||||
<div className="fifth-line" />
|
||||
<div className="fifth-line" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InboxNotePlaceholder.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default InboxNotePlaceholder;
|
|
@ -1,118 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import InboxNoteCard from '../card';
|
||||
import { getUnreadNotesCount, hasValidNotes } from '../utils';
|
||||
|
||||
jest.mock( '../action', () =>
|
||||
jest.fn().mockImplementation( () => <button>mocked button</button> )
|
||||
);
|
||||
|
||||
describe( 'InboxNoteCard', () => {
|
||||
const note = {
|
||||
id: 1,
|
||||
name: 'wc-admin-wc-helper-connection',
|
||||
type: 'info',
|
||||
title: 'Connect to WooCommerce.com',
|
||||
content: 'Connect to get important product notifications and updates.',
|
||||
status: 'unactioned',
|
||||
date_created: '2020-05-10T16:57:31',
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'connect',
|
||||
label: 'Connect',
|
||||
query: '',
|
||||
status: 'unactioned',
|
||||
primary: false,
|
||||
url: 'http://test.com',
|
||||
},
|
||||
],
|
||||
layout: 'plain',
|
||||
image: '',
|
||||
date_created_gmt: '2020-05-10T16:57:31',
|
||||
is_deleted: false,
|
||||
};
|
||||
const lastRead = 1589285995243;
|
||||
|
||||
test( 'should render a notification type banner', () => {
|
||||
note.layout = 'banner';
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const listNoteWithBanner = container.querySelector( '.banner' );
|
||||
expect( listNoteWithBanner ).not.toBeNull();
|
||||
} );
|
||||
|
||||
test( 'should render a notification type thumbnail', () => {
|
||||
note.layout = 'thumbnail';
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const listNoteWithThumbnail = container.querySelector( '.thumbnail' );
|
||||
expect( listNoteWithThumbnail ).not.toBeNull();
|
||||
} );
|
||||
|
||||
test( 'should render a read notification', () => {
|
||||
note.actions = [];
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector( '.message-is-unread' );
|
||||
const readNote = container.querySelector(
|
||||
'.woocommerce-inbox-message'
|
||||
);
|
||||
expect( unreadNote ).toBeNull();
|
||||
expect( readNote ).not.toBeNull();
|
||||
} );
|
||||
|
||||
test( 'should render an unread notification', () => {
|
||||
const olderLastRead = 1584015595000;
|
||||
note.actions = [];
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ olderLastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector( '.message-is-unread' );
|
||||
expect( unreadNote ).not.toBeNull();
|
||||
} );
|
||||
|
||||
test( 'should not render any notification', () => {
|
||||
note.is_deleted = true;
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector(
|
||||
'.woocommerce-inbox-message'
|
||||
);
|
||||
expect( unreadNote ).toBeNull();
|
||||
} );
|
||||
} );
|
||||
|
||||
const notes = [
|
||||
{
|
||||
id: 1,
|
||||
|
|
|
@ -10923,6 +10923,7 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "7.14.0",
|
||||
"@woocommerce/components": "file:packages/components",
|
||||
"@wordpress/components": "10.2.0",
|
||||
"@wordpress/element": "2.19.0",
|
||||
"@wordpress/i18n": "3.17.0",
|
||||
|
@ -10931,7 +10932,9 @@
|
|||
"classnames": "^2.3.1",
|
||||
"dompurify": "2.2.8",
|
||||
"gridicons": "3.3.1",
|
||||
"react-transition-group": "4.4.1"
|
||||
"moment": "2.29.1",
|
||||
"react-transition-group": "4.4.1",
|
||||
"react-visibility-sensor": "5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wordpress/components": {
|
||||
|
|
|
@ -13,8 +13,7 @@ import { Level } from './context';
|
|||
* (`h2`, `h3`, …) you can use `<H />` to create "section headings", which look to the parent `<Section />`s for the appropriate
|
||||
* heading level.
|
||||
*
|
||||
* @param {Object} props -
|
||||
* @return {Object} -
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
export function H( props ) {
|
||||
const level = useContext( Level );
|
||||
|
|
|
@ -12,9 +12,10 @@ import { Level } from './context';
|
|||
* The section wrapper, used to indicate a sub-section (and change the header level context).
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.component
|
||||
* @param {Node} props.children
|
||||
* @return {Object} -
|
||||
* @param {import('react').ComponentType=} props.component
|
||||
* @param {import('react').ReactNode} props.children Children to render in the tip.
|
||||
* @param {string=} props.className
|
||||
* @return {JSX.Element} -
|
||||
*/
|
||||
export function Section( { component, children, ...props } ) {
|
||||
const Component = component || 'div';
|
||||
|
@ -47,4 +48,8 @@ Section.propTypes = {
|
|||
* The children inside this section, rendered in the `component`. This increases the context level for the next heading used.
|
||||
*/
|
||||
children: PropTypes.node,
|
||||
/**
|
||||
* Optional classname
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
- Remove the use of Dashicons and replace with @wordpress/icons or gridicons #7020
|
||||
- Add expanded item text and CTA button. #6956
|
||||
- Add inbox note components (InboxNoteCard, InboxNotePlaceholder, and InboxDismissConfirmationModal). #7006
|
||||
|
||||
# 1.2.0
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"react-native": "src/index",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.14.0",
|
||||
"@woocommerce/components": "file:../components",
|
||||
"@wordpress/components": "10.2.0",
|
||||
"@wordpress/element": "2.19.0",
|
||||
"@wordpress/i18n": "3.17.0",
|
||||
|
@ -30,7 +31,9 @@
|
|||
"classnames": "^2.3.1",
|
||||
"dompurify": "2.2.8",
|
||||
"gridicons": "3.3.1",
|
||||
"react-transition-group": "4.4.1"
|
||||
"moment": "2.29.1",
|
||||
"react-transition-group": "4.4.1",
|
||||
"react-visibility-sensor": "5.1.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
|
||||
|
||||
type InboxNoteActionProps = {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a secondary button that can also be a link. If href is provided it will
|
||||
* automatically open it in a new tab/window.
|
||||
*/
|
||||
export const InboxNoteActionButton: React.FC< InboxNoteActionProps > = ( {
|
||||
label,
|
||||
onClick,
|
||||
href,
|
||||
} ) => {
|
||||
const [ inAction, setInAction ] = useState( false );
|
||||
|
||||
const handleActionClick: React.MouseEventHandler< HTMLAnchorElement > = (
|
||||
event
|
||||
) => {
|
||||
const targetHref = event.currentTarget.href || '';
|
||||
let isActionable = true;
|
||||
|
||||
if ( targetHref.length && ! targetHref.startsWith( adminUrl ) ) {
|
||||
event.preventDefault();
|
||||
isActionable = false; // link buttons shouldn't be "busy".
|
||||
window.open( targetHref, '_blank' );
|
||||
}
|
||||
|
||||
setInAction( isActionable );
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
isSecondary
|
||||
isBusy={ inAction }
|
||||
disabled={ inAction }
|
||||
href={ href }
|
||||
onClick={ handleActionClick }
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
type ConfirmationModalProps = {
|
||||
onClose: () => void;
|
||||
onDismiss: () => void;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
|
||||
export const InboxDismissConfirmationModal: React.FC< ConfirmationModalProps > = ( {
|
||||
onClose,
|
||||
onDismiss,
|
||||
buttonLabel = __( "Yes, I'm sure", 'woocommerce-admin' ),
|
||||
} ) => {
|
||||
const [ inAction, setInAction ] = useState( false );
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Are you sure?', 'woocommerce-admin' ) }
|
||||
onRequestClose={ () => onClose() }
|
||||
className="woocommerce-inbox-dismiss-confirmation_modal"
|
||||
>
|
||||
<div className="woocommerce-inbox-dismiss-confirmation_wrapper">
|
||||
<p>
|
||||
{ __(
|
||||
'Dismissed messages cannot be viewed again',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
<div className="woocommerce-inbox-dismiss-confirmation_buttons">
|
||||
<Button isSecondary onClick={ () => onClose() }>
|
||||
{ __( 'Cancel', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isSecondary
|
||||
isBusy={ inAction }
|
||||
disabled={ inAction }
|
||||
onClick={ () => {
|
||||
setInAction( true );
|
||||
onDismiss();
|
||||
} }
|
||||
>
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,291 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, useRef } from '@wordpress/element';
|
||||
import { Button, Dropdown, Popover } from '@wordpress/components';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import moment from 'moment';
|
||||
import classnames from 'classnames';
|
||||
import { H, Section } from '@woocommerce/components';
|
||||
import { sanitize } from 'dompurify';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { InboxNoteActionButton } from './action';
|
||||
import { useCallbackOnLinkClick } from './use-callback-on-link-click';
|
||||
|
||||
const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
|
||||
const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];
|
||||
|
||||
const sanitizeHTML = ( html: string ) => {
|
||||
return {
|
||||
__html: sanitize( html, { ALLOWED_TAGS, ALLOWED_ATTR } ),
|
||||
};
|
||||
};
|
||||
|
||||
type InboxNoteAction = {
|
||||
id: number;
|
||||
url: string;
|
||||
label: string;
|
||||
primary: boolean;
|
||||
actioned_text?: boolean;
|
||||
};
|
||||
|
||||
type InboxNote = {
|
||||
id: number;
|
||||
status: string;
|
||||
title: string;
|
||||
name: string;
|
||||
content: string;
|
||||
date_created: string;
|
||||
date_created_gmt: string;
|
||||
actions: InboxNoteAction[];
|
||||
layout: string;
|
||||
image: string;
|
||||
is_deleted: boolean;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type InboxNoteProps = {
|
||||
note: InboxNote;
|
||||
lastRead: number;
|
||||
onDismiss?: ( note: InboxNote, type: 'all' | 'note' ) => void;
|
||||
onNoteActionClick?: ( note: InboxNote, action: InboxNoteAction ) => void;
|
||||
onBodyLinkClick?: ( note: InboxNote, link: string ) => void;
|
||||
onNoteVisible?: ( note: InboxNote ) => void;
|
||||
};
|
||||
|
||||
const DropdownWithPopoverProps = Dropdown as React.ComponentType<
|
||||
Dropdown.Props & { popoverProps: Omit< Popover.Props, 'children' > }
|
||||
>;
|
||||
|
||||
const InboxNoteCard: React.FC< InboxNoteProps > = ( {
|
||||
note,
|
||||
lastRead,
|
||||
onDismiss,
|
||||
onNoteActionClick,
|
||||
onBodyLinkClick,
|
||||
onNoteVisible,
|
||||
} ) => {
|
||||
const [ clickedActionText, setClickedActionText ] = useState( false );
|
||||
const hasBeenSeen = useRef( false );
|
||||
const toggleButtonRef = useRef< HTMLButtonElement >( null );
|
||||
const linkCallbackRef = useCallbackOnLinkClick( ( innerLink ) => {
|
||||
if ( onBodyLinkClick ) {
|
||||
onBodyLinkClick( note, innerLink );
|
||||
}
|
||||
} );
|
||||
|
||||
// Trigger a view Tracks event when the note is seen.
|
||||
const onVisible = ( isVisible: boolean ) => {
|
||||
if ( isVisible && ! hasBeenSeen.current ) {
|
||||
if ( onNoteVisible ) {
|
||||
onNoteVisible( note );
|
||||
}
|
||||
|
||||
hasBeenSeen.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = ( event: React.FocusEvent, onClose: () => void ) => {
|
||||
const dropdownClasses = [
|
||||
'woocommerce-admin-dismiss-notification',
|
||||
'components-popover__content',
|
||||
];
|
||||
// This line is for IE compatibility.
|
||||
let relatedTarget: EventTarget | Element | null = null;
|
||||
if ( event.relatedTarget ) {
|
||||
relatedTarget = event.relatedTarget;
|
||||
} else if ( toggleButtonRef.current ) {
|
||||
const ownerDoc = toggleButtonRef.current.ownerDocument;
|
||||
relatedTarget = ownerDoc ? ownerDoc.activeElement : null;
|
||||
}
|
||||
let isClickOutsideDropdown = false;
|
||||
if ( relatedTarget && 'className' in relatedTarget ) {
|
||||
const classNames = relatedTarget.className;
|
||||
isClickOutsideDropdown = dropdownClasses.some( ( className ) =>
|
||||
classNames.includes( className )
|
||||
);
|
||||
}
|
||||
if ( isClickOutsideDropdown ) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const onDropdownDismiss = (
|
||||
type: 'note' | 'all',
|
||||
onToggle: () => void
|
||||
) => {
|
||||
if ( onDismiss ) {
|
||||
onDismiss( note, type );
|
||||
}
|
||||
onToggle();
|
||||
};
|
||||
|
||||
const renderDismissButton = () => {
|
||||
if ( clickedActionText ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownWithPopoverProps
|
||||
contentClassName="woocommerce-admin-dismiss-dropdown"
|
||||
position="bottom right"
|
||||
renderToggle={ ( { onClose, onToggle } ) => (
|
||||
<Button
|
||||
isTertiary
|
||||
onClick={ onToggle }
|
||||
ref={ toggleButtonRef }
|
||||
onBlur={ ( event: React.FocusEvent ) =>
|
||||
handleBlur( event, onClose )
|
||||
}
|
||||
>
|
||||
{ __( 'Dismiss', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
focusOnMount={ false }
|
||||
popoverProps={ { noArrow: true } }
|
||||
renderContent={ ( { onToggle } ) => (
|
||||
<ul>
|
||||
<li>
|
||||
<Button
|
||||
className="woocommerce-admin-dismiss-notification"
|
||||
onClick={ () =>
|
||||
onDropdownDismiss( 'note', onToggle )
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Dismiss this message',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
className="woocommerce-admin-dismiss-notification"
|
||||
onClick={ () =>
|
||||
onDropdownDismiss( 'all', onToggle )
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Dismiss all messages',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onActionClicked = ( action: InboxNoteAction ) => {
|
||||
if ( onNoteActionClick ) {
|
||||
onNoteActionClick( note, action );
|
||||
}
|
||||
if ( ! action.actioned_text ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setClickedActionText( action.actioned_text );
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
const { actions: noteActions, id: noteId } = note;
|
||||
|
||||
if ( !! clickedActionText ) {
|
||||
return clickedActionText;
|
||||
}
|
||||
|
||||
if ( ! noteActions ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ noteActions.map( ( action ) => (
|
||||
<InboxNoteActionButton
|
||||
key={ action.id }
|
||||
label={ action.label }
|
||||
href={
|
||||
action && action.url && action.url.length
|
||||
? action.url
|
||||
: undefined
|
||||
}
|
||||
onClick={ () => onActionClicked( action ) }
|
||||
/>
|
||||
) ) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
content,
|
||||
date_created: dateCreated,
|
||||
date_created_gmt: dateCreatedGmt,
|
||||
image,
|
||||
is_deleted: isDeleted,
|
||||
layout,
|
||||
status,
|
||||
title,
|
||||
} = note;
|
||||
|
||||
if ( isDeleted ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unread =
|
||||
! lastRead ||
|
||||
! dateCreatedGmt ||
|
||||
new Date( dateCreatedGmt + 'Z' ).getTime() > lastRead;
|
||||
const date = dateCreated;
|
||||
const hasImage = layout !== 'plain' && layout !== '';
|
||||
const cardClassName = classnames( 'woocommerce-inbox-message', layout, {
|
||||
'message-is-unread': unread && status === 'unactioned',
|
||||
} );
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={ onVisible }>
|
||||
<section className={ cardClassName }>
|
||||
{ hasImage && (
|
||||
<div className="woocommerce-inbox-message__image">
|
||||
<img src={ image } alt="" />
|
||||
</div>
|
||||
) }
|
||||
<div className="woocommerce-inbox-message__wrapper">
|
||||
<div className="woocommerce-inbox-message__content">
|
||||
{ unread && (
|
||||
<div className="woocommerce-inbox-message__unread-indicator" />
|
||||
) }
|
||||
{ date && (
|
||||
<span className="woocommerce-inbox-message__date">
|
||||
{ moment.utc( date ).fromNow() }
|
||||
</span>
|
||||
) }
|
||||
<H className="woocommerce-inbox-message__title">
|
||||
{ title }
|
||||
</H>
|
||||
<Section className="woocommerce-inbox-message__text">
|
||||
<span
|
||||
dangerouslySetInnerHTML={ sanitizeHTML(
|
||||
content
|
||||
) }
|
||||
ref={ linkCallbackRef }
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__actions">
|
||||
{ renderActions() }
|
||||
{ renderDismissButton() }
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</VisibilitySensor>
|
||||
);
|
||||
};
|
||||
|
||||
export { InboxNoteCard, InboxNote, InboxNoteAction };
|
|
@ -0,0 +1,3 @@
|
|||
export * from './inbox-note';
|
||||
export * from './inbox-dismiss-confirmation-modal';
|
||||
export * from './placeholder';
|
|
@ -0,0 +1,36 @@
|
|||
type PlaceholderProps = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
const InboxNotePlaceholder: React.FC< PlaceholderProps > = ( {
|
||||
className,
|
||||
} ) => {
|
||||
return (
|
||||
<div
|
||||
className={ `woocommerce-inbox-message is-placeholder ${ className }` }
|
||||
aria-hidden
|
||||
>
|
||||
<div className="woocommerce-inbox-message__wrapper">
|
||||
<div className="woocommerce-inbox-message__content">
|
||||
<div className="woocommerce-inbox-message__date">
|
||||
<div className="sixth-line" />
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__title">
|
||||
<div className="line" />
|
||||
<div className="line" />
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__text">
|
||||
<div className="line" />
|
||||
<div className="third-line" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-inbox-message__actions">
|
||||
<div className="fifth-line" />
|
||||
<div className="fifth-line" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { InboxNotePlaceholder };
|
|
@ -174,22 +174,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-homepage-notes-wrapper {
|
||||
padding-top: $gap-large;
|
||||
}
|
||||
|
||||
#activity-panel-inbox {
|
||||
margin: 0 $gap-large;
|
||||
}
|
||||
|
||||
.woocommerce-layout__inbox-panel-header {
|
||||
padding: $gap-large;
|
||||
|
||||
.woocommerce-homepage-column & {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tweak to fix dropdown and placeholder in IE 11
|
||||
@media all and ( -ms-high-contrast: none ), ( -ms-high-contrast: active ) {
|
||||
.woocommerce-admin-dismiss-dropdown {
|
||||
|
@ -202,29 +186,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-enter {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-enter-active {
|
||||
opacity: 1;
|
||||
max-height: 100vh;
|
||||
transform: translateX(0%);
|
||||
transition: opacity 500ms, transform 500ms, max-height 500ms;
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-exit {
|
||||
opacity: 1;
|
||||
max-height: 100vh;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.woocommerce-inbox-message-exit-active {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateX(50%);
|
||||
transition: opacity 500ms, transform 500ms, max-height 500ms;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { InboxDismissConfirmationModal } from '../inbox-dismiss-confirmation-modal';
|
||||
|
||||
describe( 'InboxDismissConfirmationModal', () => {
|
||||
it( "should render with default button label - Yes, I'am sure", () => {
|
||||
const { queryByText } = render(
|
||||
<InboxDismissConfirmationModal
|
||||
onClose={ jest.fn() }
|
||||
onDismiss={ jest.fn() }
|
||||
/>
|
||||
);
|
||||
expect( queryByText( "Yes, I'm sure" ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render passed in button label if provided', () => {
|
||||
const { queryByText } = render(
|
||||
<InboxDismissConfirmationModal
|
||||
onClose={ jest.fn() }
|
||||
onDismiss={ jest.fn() }
|
||||
buttonLabel="Custom button"
|
||||
/>
|
||||
);
|
||||
expect( queryByText( 'Custom button' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should call onClose if Cancel is clicked', () => {
|
||||
const onClose = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxDismissConfirmationModal
|
||||
onClose={ onClose }
|
||||
onDismiss={ jest.fn() }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Cancel' ) );
|
||||
expect( onClose ).toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'should call onDismiss if dismiss button is clicked', () => {
|
||||
const onDismiss = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxDismissConfirmationModal
|
||||
onClose={ jest.fn() }
|
||||
onDismiss={ onDismiss }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( "Yes, I'm sure" ) );
|
||||
expect( onDismiss ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,258 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { InboxNoteCard } from '../inbox-note';
|
||||
|
||||
jest.mock( 'react-visibility-sensor', () =>
|
||||
jest.fn().mockImplementation( ( { children, onChange } ) => {
|
||||
return (
|
||||
<>
|
||||
<button onClick={ () => onChange( true ) }>
|
||||
Trigger change
|
||||
</button>
|
||||
{ children }
|
||||
</>
|
||||
);
|
||||
} )
|
||||
);
|
||||
|
||||
describe( 'InboxNoteCard', () => {
|
||||
const note = {
|
||||
id: 1,
|
||||
name: 'wc-admin-wc-helper-connection',
|
||||
type: 'info',
|
||||
title: 'Connect to WooCommerce.com',
|
||||
content: 'Connect to get important product notifications and updates.',
|
||||
status: 'unactioned',
|
||||
date_created: '2020-05-10T16:57:31',
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'connect',
|
||||
label: 'Connect',
|
||||
query: '',
|
||||
status: 'unactioned',
|
||||
primary: false,
|
||||
url: 'http://test.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'learnmore',
|
||||
label: 'Learn More',
|
||||
query: '',
|
||||
status: 'unactioned',
|
||||
primary: false,
|
||||
url: 'http://test.com',
|
||||
},
|
||||
],
|
||||
layout: 'plain',
|
||||
image: '',
|
||||
date_created_gmt: '2020-05-10T16:57:31',
|
||||
is_deleted: false,
|
||||
};
|
||||
const lastRead = 1589285995243;
|
||||
|
||||
it( 'should render the defined action buttons', () => {
|
||||
const { queryByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
expect( queryByText( 'Connect' ) ).toBeInTheDocument();
|
||||
expect( queryByText( 'Learn More' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render a dismiss button', () => {
|
||||
const { queryByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
expect( queryByText( 'Dismiss' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render a notification type banner', () => {
|
||||
const bannerNote = { ...note, layout: 'banner' };
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ bannerNote.id }
|
||||
note={ bannerNote }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const listNoteWithBanner = container.querySelector( '.banner' );
|
||||
expect( listNoteWithBanner ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should render a notification type thumbnail', () => {
|
||||
const thumbnailNote = { ...note, layout: 'thumbnail' };
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ thumbnailNote.id }
|
||||
note={ thumbnailNote }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const listNoteWithThumbnail = container.querySelector( '.thumbnail' );
|
||||
expect( listNoteWithThumbnail ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should render a read notification', () => {
|
||||
const noteWithoutActions = { ...note, actions: [] };
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ noteWithoutActions }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector( '.message-is-unread' );
|
||||
const readNote = container.querySelector(
|
||||
'.woocommerce-inbox-message'
|
||||
);
|
||||
expect( unreadNote ).toBeNull();
|
||||
expect( readNote ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should render an unread notification', () => {
|
||||
const olderLastRead = 1584015595000;
|
||||
const noteWithoutActions = { ...note, actions: [] };
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ noteWithoutActions }
|
||||
lastRead={ olderLastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector( '.message-is-unread' );
|
||||
expect( unreadNote ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should not render any notification', () => {
|
||||
const deletedNote = { ...note, is_deleted: true };
|
||||
const { container } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ deletedNote }
|
||||
lastRead={ lastRead }
|
||||
/>
|
||||
);
|
||||
const unreadNote = container.querySelector(
|
||||
'.woocommerce-inbox-message'
|
||||
);
|
||||
expect( unreadNote ).toBeNull();
|
||||
} );
|
||||
|
||||
describe( 'callbacks', () => {
|
||||
it( 'should call onDismiss with note type when "Dismiss this message" is clicked', () => {
|
||||
const onDismiss = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onDismiss={ onDismiss }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Dismiss' ) );
|
||||
userEvent.click( getByText( 'Dismiss this message' ) );
|
||||
expect( onDismiss ).toHaveBeenCalledWith( note, 'note' );
|
||||
} );
|
||||
|
||||
it( 'should call onDismiss with all type when "Dismiss all messages" is clicked', () => {
|
||||
const onDismiss = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onDismiss={ onDismiss }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Dismiss' ) );
|
||||
userEvent.click( getByText( 'Dismiss all messages' ) );
|
||||
expect( onDismiss ).toHaveBeenCalledWith( note, 'all' );
|
||||
} );
|
||||
|
||||
it( 'should call onNoteActionClick with specific action when action is clicked', () => {
|
||||
const onNoteActionClick = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onNoteActionClick={ onNoteActionClick }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Learn More' ) );
|
||||
expect( onNoteActionClick ).toHaveBeenCalledWith(
|
||||
note,
|
||||
note.actions[ 1 ]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should call onBodyLinkClick with innerLink if link within content is clicked', () => {
|
||||
const onBodyLinkClick = jest.fn();
|
||||
const noteWithInnerLink = {
|
||||
...note,
|
||||
content:
|
||||
note.content +
|
||||
' <a href="http://somewhere.com">Somewhere</a>',
|
||||
};
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ noteWithInnerLink.id }
|
||||
note={ noteWithInnerLink }
|
||||
lastRead={ lastRead }
|
||||
onBodyLinkClick={ onBodyLinkClick }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Somewhere' ) );
|
||||
expect( onBodyLinkClick ).toHaveBeenCalledWith(
|
||||
noteWithInnerLink,
|
||||
'http://somewhere.com/'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should call onVisible when visiblity sensor calls it', () => {
|
||||
const onVisible = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onNoteVisible={ onVisible }
|
||||
/>
|
||||
);
|
||||
expect( onVisible ).not.toHaveBeenCalled();
|
||||
userEvent.click( getByText( 'Trigger change' ) );
|
||||
expect( onVisible ).toHaveBeenCalledWith( note );
|
||||
} );
|
||||
|
||||
it( 'should call onVisible when visiblity sensor calls it, but only once', () => {
|
||||
const onVisible = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
key={ note.id }
|
||||
note={ note }
|
||||
lastRead={ lastRead }
|
||||
onNoteVisible={ onVisible }
|
||||
/>
|
||||
);
|
||||
userEvent.click( getByText( 'Trigger change' ) );
|
||||
userEvent.click( getByText( 'Trigger change' ) );
|
||||
userEvent.click( getByText( 'Trigger change' ) );
|
||||
expect( onVisible ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCallbackOnLinkClick } from '../use-callback-on-link-click';
|
||||
|
||||
const TestComp = ( { callback }: { callback: ( link: string ) => void } ) => {
|
||||
const containerRef = useCallbackOnLinkClick( ( link ) => {
|
||||
callback( link );
|
||||
} );
|
||||
|
||||
return (
|
||||
<span ref={ containerRef }>
|
||||
Some Text
|
||||
<br />
|
||||
<span>
|
||||
Inner paragraph
|
||||
<a href="http://tosomewhere.com">Link</a>
|
||||
</span>
|
||||
<button>Button</button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
describe( 'useCallbackOnLinkClick hook', () => {
|
||||
it( 'should call callback with link when inner anchor element is clicked', () => {
|
||||
const callback = jest.fn();
|
||||
const { getByText } = render( <TestComp callback={ callback } /> );
|
||||
userEvent.click( getByText( 'Link' ) );
|
||||
expect( callback ).toHaveBeenCalledWith( 'http://tosomewhere.com/' );
|
||||
} );
|
||||
|
||||
it( 'should not call callback if click event target does not have an href', () => {
|
||||
const callback = jest.fn();
|
||||
const { getByText } = render( <TestComp callback={ callback } /> );
|
||||
userEvent.click( getByText( 'Button' ) );
|
||||
expect( callback ).not.toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) {
|
||||
const onNodeClick = useCallback(
|
||||
( event: MouseEvent ): void => {
|
||||
const target = event.target as
|
||||
| EventTarget
|
||||
| HTMLAnchorElement
|
||||
| null;
|
||||
if ( target && 'href' in target ) {
|
||||
const innerLink = target.href;
|
||||
if ( innerLink && onClick ) {
|
||||
onClick( innerLink );
|
||||
}
|
||||
}
|
||||
},
|
||||
[ onClick ]
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
( node: HTMLElement ) => {
|
||||
if ( node ) {
|
||||
node.addEventListener( 'click', onNodeClick );
|
||||
}
|
||||
return () => {
|
||||
if ( node ) {
|
||||
node.removeEventListener( 'click', onNodeClick );
|
||||
}
|
||||
};
|
||||
},
|
||||
[ onNodeClick ]
|
||||
);
|
||||
}
|
|
@ -37,3 +37,4 @@ export { ExperimentalListItem as ListItem } from './experimental-list/experiment
|
|||
export { ExperimentalList as List } from './experimental-list/experimental-list';
|
||||
export { ExperimentalCollapsibleList as CollapsibleList } from './experimental-list/collapsible-list';
|
||||
export { TaskItem } from './experimental-list/task-item';
|
||||
export * from './inbox-note';
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
@import 'experimental-list/style.scss';
|
||||
@import 'experimental-list/collapsible-list/style.scss';
|
||||
@import 'experimental-list/task-item.scss';
|
||||
@import 'inbox-note/style.scss';
|
||||
|
|
|
@ -109,6 +109,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
|
|||
- Dev: Offload remote inbox notifications engine run using action-scheduler. #6995
|
||||
- Dev: Add source param support for notes query. #6979
|
||||
- Dev: Remove the use of Dashicons and replace with @wordpress/icons or gridicons. #7020
|
||||
- Dev: Refactor inbox panel components and moved to experimental package. #7006
|
||||
- Enhancement: Add expand/collapse to extendable task list. #6910
|
||||
- Enhancement: Add task hierarchy support to extended task list. #6916
|
||||
- Fix: Rule Processing Transformer to handle dotNotation default value #7009
|
||||
|
|
Loading…
Reference in New Issue