Skip to content

Commit

Permalink
feat: add component to iframe LTI launch (#1135)
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto authored Jul 6, 2023
1 parent 714f5d4 commit 2293791
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 3 deletions.
10 changes: 9 additions & 1 deletion src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import ChatTrigger from './lti-modal/ChatTrigger';

import { useModel } from '../../generic/model-store';
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
Expand Down Expand Up @@ -91,7 +92,14 @@ const Course = ({
unitId={unitId}
/>
{shouldDisplayTriggers && (
<SidebarTriggers />
<>
<ChatTrigger
enrollmentMode={course.enrollmentMode}
isStaff={isStaff}
launchUrl={course.learningAssistantLaunchUrl}
/>
<SidebarTriggers />
</>
)}
</div>

Expand Down
122 changes: 122 additions & 0 deletions src/courseware/course/lti-modal/ChatTrigger.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
Icon,
useToggle,
OverlayTrigger,
Popover,
} from '@edx/paragon';
import { ChatBubbleOutline } from '@edx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import messages from './messages';

const ChatTrigger = ({
intl,
enrollmentMode,
isStaff,
launchUrl,
}) => {
const [isOpen, open, close] = useToggle(false);
const [hasOpenedChat, setHasOpenedChat] = useState(false);

const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
];

const isVerifiedEnrollmentMode = (
enrollmentMode !== null
&& enrollmentMode !== undefined
&& VERIFIED_MODES.some(mode => mode === enrollmentMode)
);

const shouldDisplayChat = (
launchUrl
&& (isVerifiedEnrollmentMode || isStaff) // display only to non-audit or staff
);

const handleOpen = () => {
if (!hasOpenedChat) {
setHasOpenedChat(true);
}
open();
};

return (
<>
{shouldDisplayChat && (
<div
className={classNames('mt-3', 'd-flex', 'ml-auto')}
>
<OverlayTrigger
trigger="click"
key="top"
show={!hasOpenedChat}
overlay={(
<Popover id="popover-chat-information">
<Popover.Title as="h3">{intl.formatMessage(messages.popoverTitle)}</Popover.Title>
<Popover.Content>
{intl.formatMessage(messages.popoverContent)}
</Popover.Content>
</Popover>
)}
>
<button
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex"
type="button"
onClick={handleOpen}
aria-label={intl.formatMessage(messages.openChatModalTrigger)}
>
<div className="icon-container d-flex position-relative align-items-center">
<Icon src={ChatBubbleOutline} className="m-0 m-auto" />
</div>
</button>
</OverlayTrigger>
<ModalDialog
onClose={close}
isOpen={isOpen}
title={intl.formatMessage(messages.modalTitle)}
size="xl"
hasCloseButton
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.modalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<iframe
src={launchUrl}
allowFullScreen
style={{
width: '100%',
height: '60vh',
}}
title={intl.formatMessage(messages.modalTitle)}
/>
</ModalDialog.Body>
</ModalDialog>
</div>
)}
</>
);
};

ChatTrigger.propTypes = {
intl: intlShape.isRequired,
isStaff: PropTypes.bool.isRequired,
enrollmentMode: PropTypes.string.isRequired,
launchUrl: PropTypes.string,
};

ChatTrigger.defaultProps = {
launchUrl: null,
};

export default injectIntl(ChatTrigger);
74 changes: 74 additions & 0 deletions src/courseware/course/lti-modal/ChatTrigger.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { BrowserRouter } from 'react-router-dom';
import React from 'react';
import ChatTrigger from './ChatTrigger';
import { act, fireEvent, screen } from '../../../setupTest';

describe('ChatTrigger', () => {
it('handles click to open/close chat modal', async () => {
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={null}
isStaff
launchUrl="https://testurl.org"
/>
</BrowserRouter>,
</IntlProvider>,
);

const chatTrigger = screen.getByRole('button', { name: /Show chat modal/i });
expect(chatTrigger).toBeInTheDocument();
expect(screen.queryByText('Need help understanding course content?')).toBeInTheDocument();

await act(async () => {
fireEvent.click(chatTrigger);
});
const modalCloseButton = screen.getByRole('button', { name: /Close/i });
await expect(modalCloseButton).toBeInTheDocument();

await act(async () => {
fireEvent.click(modalCloseButton);
});
await expect(modalCloseButton).not.toBeInTheDocument();
expect(screen.queryByText('Holy guacamole!')).not.toBeInTheDocument();
});

const testCases = [
{ enrollmentMode: null, isVisible: false },
{ enrollmentMode: undefined, isVisible: false },
{ enrollmentMode: 'audit', isVisible: false },
{ enrollmentMode: 'xyz', isVisible: false },
{ enrollmentMode: 'professional', isVisible: true },
{ enrollmentMode: 'verified', isVisible: true },
{ enrollmentMode: 'no-id-professional', isVisible: true },
{ enrollmentMode: 'credit', isVisible: true },
{ enrollmentMode: 'masters', isVisible: true },
{ enrollmentMode: 'executive-education', isVisible: true },
];

testCases.forEach(test => {
it(`does chat to be visible based on enrollment mode of ${test.enrollmentMode}`, async () => {
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={test.enrollmentMode}
isStaff={false}
launchUrl="https://testurl.org"
/>
</BrowserRouter>,
</IntlProvider>,
);

const chatTrigger = screen.queryByRole('button', { name: /Show chat modal/i });
if (test.isVisible) {
expect(chatTrigger).toBeInTheDocument();
} else {
expect(chatTrigger).not.toBeInTheDocument();
}
});
});
});
1 change: 1 addition & 0 deletions src/courseware/course/lti-modal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ChatTrigger';
26 changes: 26 additions & 0 deletions src/courseware/course/lti-modal/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
popoverTitle: {
id: 'popover.title',
defaultMessage: 'Need help understanding course content?',
description: 'Title for popover alerting user of chat modal',
},
popoverContent: {
id: 'popover.content',
defaultMessage: 'Click here for your Xpert Learning Assistant.',
description: 'Content of the popover message',
},
openChatModalTrigger: {
id: 'chat.model.trigger',
defaultMessage: 'Show chat modal',
description: 'Alt text for button that opens the chat modal',
},
modalTitle: {
id: 'chat.model.title',
defaultMessage: 'Xpert Learning Assistant',
description: 'Title for chat modal header',
},
});

export default messages;
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ Factory.define('courseMetadata')
related_programs: null,
user_needs_integrity_signature: false,
recommendations: null,
learning_assistant_launch_url: null,
});
1 change: 1 addition & 0 deletions src/courseware/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function normalizeMetadata(metadata) {
relatedPrograms: camelCaseObject(data.related_programs),
userNeedsIntegritySignature: data.user_needs_integrity_signature,
canAccessProctoredExams: data.can_access_proctored_exams,
learningAssistantLaunchUrl: data.learning_assistant_launch_url,
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/courseware/data/pact-tests/lmsPact.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ describe('Courseware Service', () => {
linkedinAddToProfileUrl: null,
relatedPrograms: null,
userNeedsIntegritySignature: false,
learningAssistantLaunchUrl: null,
};
setTimeout(() => {
provider.addInteraction({
Expand Down Expand Up @@ -338,6 +339,7 @@ describe('Courseware Service', () => {
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
learning_assistant_launch_url: null,
},
},
});
Expand Down
8 changes: 6 additions & 2 deletions src/pacts/frontend-app-learning-lms.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,14 @@
"course_exit_page_is_active": false,
"certificate_data": {
"cert_status": "audit_passing",
"cert_web_view_url": null,
"cert_web_view_url": null,
"certificate_available_date": null
},
"verify_identity_url": null,
"verification_status": "none",
"linkedin_add_to_profile_url": null,
"user_needs_integrity_signature": false
"user_needs_integrity_signature": false,
"learning_assistant_launch_url": null
},
"matchingRules": {
"$.body.access_expiration.expiration_date": {
Expand Down Expand Up @@ -440,6 +441,9 @@
},
"$.body.user_needs_integrity_signature": {
"match": "type"
},
"$.body.learning_assistant_launch_url": {
"match": "type"
}
}
}
Expand Down

0 comments on commit 2293791

Please sign in to comment.