Skip to content

Commit a66ec89

Browse files
feat(insights): Added support for Advanced Content Insights (#3116)
* feat(insights): Added support for Advanced Content Insights * feat(insights): Added feedback from comments * feat(insights): Changed class and var names * feat(insights): Added feedback from comments * feat(insights): Updated tests and Bar components * feat(insights): Removed wrapper component around toggle * feat(insights): Refactor component props * feat(insights): Added feedback from comments - Change ContentPreview test to make it more simple - Removed the flowTypes from advanced content insights - Fix an issue with reference object comparison instead of deep comparison * feat(insights): refactor options * feat(insights): change property name * refactor(insights): Refactor conditional options on USM and ModalDialog * refactor(insights): Address feddback from comments - Change Variable name from isMainUSMSectionShown to hasExpandedSections and inverted boolean logic - Modify onRequestClose on ModalDialog to follow the same approach we are using onRequestBack
1 parent 18b8ea6 commit a66ec89

File tree

19 files changed

+546
-30
lines changed

19 files changed

+546
-30
lines changed

i18n/en-US.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,12 @@ boxui.accessStats.accessStatsPreviews = Previews
804804
boxui.accessStats.accessStatsViewDetails = View Details
805805
# The label for the view category of access stats for box notes
806806
boxui.accessStats.accessStatsViews = Views
807+
# Description text about advanced content insights.
808+
boxui.advancedContentInsights.advancedContentInsightsDescription = Get actionable insights into how viewers are engaging with this content. Measure average time spent, page-by-page engagement, per person and per visit details. {helpLink}
809+
# Advanced Content Insights toggle title
810+
boxui.advancedContentInsights.advancedContentInsightsTitle = Advanced Insights
811+
# Text for a link that goes to an external page with more information about Advanced Content Insights
812+
boxui.advancedContentInsights.learnMore = Learn more.
807813
# Text for the beta badge
808814
boxui.badges.beta = BETA
809815
# Text for the trial badge
@@ -1134,6 +1140,8 @@ boxui.metadataInstanceFields.fieldSetDate = Set Date
11341140
boxui.metadataInstanceFields.fieldSetValue = Set Value
11351141
# Error message displayed if the template has an field type we don't recognize
11361142
boxui.metadataInstanceFields.invalidMetadataFieldType = Invalid metadata field type!
1143+
# Button to get back inside modal
1144+
boxui.modalDialog.backModalText = Back
11371145
# Button to close modal
11381146
boxui.modalDialog.closeModalText = Close Modal
11391147
# Text shown on button to close the modal used to create a new folder

src/components/modal/Modal.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@
8282
margin-top: 20px;
8383
}
8484

85+
.modal-back-button {
86+
display: flex;
87+
align-items: center;
88+
justify-content: center;
89+
padding: 0;
90+
background: none;
91+
border: 0;
92+
transform: rotate(180deg);
93+
cursor: pointer;
94+
}
95+
96+
.modal-back-button + .modal-title {
97+
margin-left: 12px;
98+
}
99+
85100
.modal-close-button {
86101
position: absolute;
87102
top: 20px;

src/components/modal/ModalDialog.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import omit from 'lodash/omit';
55
import uniqueId from 'lodash/uniqueId';
66
import { defineMessages, injectIntl } from 'react-intl';
77

8+
import IconBack from '../../icon/fill/Arrow16';
89
import IconClose from '../../icon/fill/X16';
910

1011
const ALERT_TYPE = 'alert';
1112
const DIALOG_TYPE = 'dialog';
1213

1314
const messages = defineMessages({
15+
backModalText: {
16+
defaultMessage: 'Back',
17+
description: 'Button to get back inside modal',
18+
id: 'boxui.modalDialog.backModalText',
19+
},
1420
closeModalText: {
1521
defaultMessage: 'Close Modal',
1622
description: 'Button to close modal',
@@ -24,6 +30,7 @@ type Props = {
2430
closeButtonProps: Object,
2531
intl: Object,
2632
modalRef?: Function,
33+
onRequestBack?: Function,
2734
onRequestClose?: Function,
2835
title?: React.Node,
2936
type?: 'alert' | 'dialog',
@@ -35,6 +42,18 @@ class ModalDialog extends React.Component<Props> {
3542
closeButtonProps: {},
3643
};
3744

45+
/**
46+
* Handles clicking on the back button
47+
* @param {SyntheticMouseEvent} event
48+
* @return {void}
49+
*/
50+
onBackButtonClick = (event: SyntheticMouseEvent<HTMLButtonElement>) => {
51+
const { onRequestBack } = this.props;
52+
if (onRequestBack) {
53+
onRequestBack(event);
54+
}
55+
};
56+
3857
/**
3958
* Handles clicking on the close button
4059
* @param {SyntheticMouseEvent} event
@@ -49,16 +68,33 @@ class ModalDialog extends React.Component<Props> {
4968

5069
modalID: string = uniqueId('modal');
5170

71+
/**
72+
* Renders a button if onRequestBack is passed in
73+
* @return {ReactElement|null} - Returns the button, or null if the button shouldn't be rendered
74+
*/
75+
renderBackButton() {
76+
const { intl } = this.props;
77+
const { formatMessage } = intl;
78+
return (
79+
<button
80+
aria-label={formatMessage(messages.backModalText)}
81+
className="modal-back-button"
82+
data-testid="modal-back-button"
83+
onClick={this.onBackButtonClick}
84+
type="button"
85+
>
86+
<IconBack height={18} width={18} />
87+
</button>
88+
);
89+
}
90+
5291
/**
5392
* Renders a button if onRequestClose is passed in
5493
* @return {ReactElement|null} - Returns the button, or null if the button shouldn't be rendered
5594
*/
5695
renderCloseButton() {
57-
const { closeButtonProps, onRequestClose, intl } = this.props;
96+
const { closeButtonProps, intl } = this.props;
5897
const { formatMessage } = intl;
59-
if (!onRequestClose) {
60-
return null;
61-
}
6298

6399
return (
64100
// eslint-disable-next-line react/button-has-type
@@ -97,6 +133,8 @@ class ModalDialog extends React.Component<Props> {
97133
const {
98134
className,
99135
modalRef,
136+
onRequestBack,
137+
onRequestClose,
100138
title,
101139
type,
102140
...rest // Useful for resin tagging, and other misc tags such as a11y
@@ -114,11 +152,12 @@ class ModalDialog extends React.Component<Props> {
114152
<div ref={modalRef} className={classNames('modal-dialog', className)} {...divProps}>
115153
<div className="modal-header-container">
116154
<div className="modal-header">
155+
{onRequestBack && this.renderBackButton()}
117156
<h2 className="modal-title" id={`${this.modalID}-label`}>
118157
{title}
119158
</h2>
120159
</div>
121-
{this.renderCloseButton()}
160+
{onRequestClose && this.renderCloseButton()}
122161
</div>
123162
{this.renderContent()}
124163
</div>

src/components/modal/__tests__/ModalDialog.test.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ModalDialogBase } from '../ModalDialog';
66
const sandbox = sinon.sandbox.create();
77

88
describe('components/modal/ModalDialog', () => {
9+
let onRequestBack;
910
let onRequestClose;
1011
let wrapper;
1112
let instance;
@@ -16,8 +17,14 @@ describe('components/modal/ModalDialog', () => {
1617
formatMessage: message => message.id,
1718
};
1819
onRequestClose = sinon.spy();
20+
onRequestBack = sinon.spy();
1921
wrapper = shallow(
20-
<ModalDialogBase intl={intlShape} onRequestClose={onRequestClose} title={title}>
22+
<ModalDialogBase
23+
intl={intlShape}
24+
onRequestBack={onRequestBack}
25+
onRequestClose={onRequestClose}
26+
title={title}
27+
>
2128
children
2229
</ModalDialogBase>,
2330
);
@@ -68,4 +75,18 @@ describe('components/modal/ModalDialog', () => {
6875
wrapper.setProps({ onRequestClose: undefined });
6976
expect(wrapper.find('.modal-close-button').length).toBeFalsy();
7077
});
78+
79+
test('render back button when onRequestBack is defined', () => {
80+
expect(wrapper.find('.modal-back-button').length).toBeTruthy();
81+
});
82+
83+
test('should not render back button if onRequestBack is null', () => {
84+
wrapper.setProps({ onRequestBack: null });
85+
expect(wrapper.find('.modal-back-button').length).toBeFalsy();
86+
});
87+
88+
test('should call onRequestBack when back button is clicked on', () => {
89+
wrapper.find('.modal-back-button').simulate('click');
90+
sinon.assert.calledOnce(onRequestBack);
91+
});
7192
});

src/elements/content-preview/ContentPreview.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import 'regenerator-runtime/runtime';
88
import * as React from 'react';
99
import classNames from 'classnames';
10-
import uniqueid from 'lodash/uniqueId';
11-
import throttle from 'lodash/throttle';
1210
import cloneDeep from 'lodash/cloneDeep';
13-
import omit from 'lodash/omit';
14-
import getProp from 'lodash/get';
1511
import flow from 'lodash/flow';
12+
import getProp from 'lodash/get';
13+
import isEqual from 'lodash/isEqual';
1614
import noop from 'lodash/noop';
15+
import omit from 'lodash/omit';
1716
import setProp from 'lodash/set';
17+
import throttle from 'lodash/throttle';
18+
import uniqueid from 'lodash/uniqueId';
1819
import Measure from 'react-measure';
1920
import { withRouter } from 'react-router-dom';
2021
import type { ContextRouter } from 'react-router-dom';
@@ -75,6 +76,11 @@ type StartAt = {
7576
};
7677

7778
type Props = {
79+
advancedContentInsights: {
80+
isActive: boolean,
81+
ownerEId: number,
82+
userId: number,
83+
},
7884
apiHost: string,
7985
appHost: string,
8086
autoFocus: boolean,
@@ -100,6 +106,7 @@ type Props = {
100106
onAnnotator: Function,
101107
onAnnotatorEvent: Function,
102108
onClose?: Function,
109+
onContentInsightsEventReport: Function,
103110
onDownload: Function,
104111
onLoad: Function,
105112
onNavigate: Function,
@@ -229,6 +236,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
229236
language: DEFAULT_LOCALE,
230237
onAnnotator: noop,
231238
onAnnotatorEvent: noop,
239+
onContentInsightsEventReport: noop,
232240
onDownload: noop,
233241
onError: noop,
234242
onLoad: noop,
@@ -362,11 +370,16 @@ class ContentPreview extends React.PureComponent<Props, State> {
362370
* @return {void}
363371
*/
364372
componentDidUpdate(prevProps: Props, prevState: State): void {
365-
const { previewExperiences, token } = this.props;
366-
const { previewExperiences: prevPreviewExperiences, token: prevToken } = prevProps;
373+
const { advancedContentInsights, previewExperiences, token } = this.props;
374+
const {
375+
advancedContentInsights: prevContentInsightsOptions,
376+
previewExperiences: prevPreviewExperiences,
377+
token: prevToken,
378+
} = prevProps;
367379
const { currentFileId } = this.state;
368380
const hasFileIdChanged = prevState.currentFileId !== currentFileId;
369381
const hasTokenChanged = prevToken !== token;
382+
const haveContentInsightsChanged = !isEqual(prevContentInsightsOptions, advancedContentInsights);
370383
const haveExperiencesChanged = prevPreviewExperiences !== previewExperiences;
371384

372385
if (hasFileIdChanged) {
@@ -384,6 +397,10 @@ class ContentPreview extends React.PureComponent<Props, State> {
384397
if (haveExperiencesChanged && this.preview && this.preview.updateExperiences) {
385398
this.preview.updateExperiences(previewExperiences);
386399
}
400+
401+
if (advancedContentInsights && haveContentInsightsChanged && this.preview?.updateContentInsightsOptions) {
402+
this.preview.updateContentInsightsOptions(advancedContentInsights);
403+
}
387404
}
388405

389406
/**
@@ -750,11 +767,13 @@ class ContentPreview extends React.PureComponent<Props, State> {
750767
*/
751768
loadPreview = async (): Promise<void> => {
752769
const {
770+
advancedContentInsights,
753771
annotatorState: { activeAnnotationId } = {},
754772
enableThumbnailsSidebar,
755773
fileOptions,
756774
onAnnotatorEvent,
757775
onAnnotator,
776+
onContentInsightsEventReport,
758777
previewExperiences,
759778
showAnnotationsControls,
760779
token: tokenOrTokenFunction,
@@ -789,6 +808,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
789808
}
790809

791810
const previewOptions = {
811+
advancedContentInsights,
792812
container: `#${this.id} .bcpr-content`,
793813
enableThumbnailsSidebar,
794814
fileOptions: fileOpts,
@@ -820,6 +840,9 @@ class ContentPreview extends React.PureComponent<Props, State> {
820840
...previewOptions,
821841
...omit(rest, Object.keys(previewOptions)),
822842
});
843+
if (advancedContentInsights) {
844+
this.preview.addListener('advanced_insights_report', onContentInsightsEventReport);
845+
}
823846
};
824847

825848
/**
@@ -1210,6 +1233,20 @@ class ContentPreview extends React.PureComponent<Props, State> {
12101233
}
12111234
}
12121235

1236+
/**
1237+
* Fetches a thumbnail for the page given
1238+
*
1239+
* @return {Promise|null} - promise resolves with the image HTMLElement or null if generation is in progress
1240+
*/
1241+
getThumbnail = (pageNumber: ?number): Promise<HTMLElement> | null => {
1242+
const preview = this.getPreview();
1243+
if (preview && preview.viewer) {
1244+
return preview.viewer.getThumbnail(pageNumber);
1245+
}
1246+
1247+
return null;
1248+
};
1249+
12131250
/**
12141251
* Renders the file preview
12151252
*

0 commit comments

Comments
 (0)