Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/pages-and-resources/PagesAndResources.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ import { useDispatch, useSelector } from 'react-redux';
import { Button, Hyperlink } from '@edx/paragon';
import messages from './messages';
import DiscussionsSettings from './discussions';
import { XpertUnitSummarySettings, fetchXpertPluginConfigurable, appInfo } from './xpert-unit-summary';

import PageGrid from './pages/PageGrid';
import { fetchCourseApps } from './data/thunks';
import { useModels } from '../generic/model-store';
import { useModels, useModel } from '../generic/model-store';
import { getCourseAppsApiStatus, getLoadingStatus } from './data/selectors';
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
import { RequestStatus } from '../data/constants';
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';

const permissonPages = [appInfo];
const PagesAndResources = ({ courseId, intl }) => {
const { path, url } = useRouteMatch();

const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCourseApps(courseId));
dispatch(fetchXpertPluginConfigurable(courseId));
}, [courseId]);

const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
Expand All @@ -35,6 +38,8 @@ const PagesAndResources = ({ courseId, intl }) => {

// Each page here is driven by a course app
const pages = useModels('courseApps', courseAppIds);
const xpertPluginConfigurable = useModel('XpertSettings.enabled', 'xpert-unit-summary');

if (loadingStatus === RequestStatus.IN_PROGRESS) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
Expand Down Expand Up @@ -62,6 +67,18 @@ const PagesAndResources = ({ courseId, intl }) => {
</div>

<PageGrid pages={pages} />

{
xpertPluginConfigurable?.enabled ? (
<>
<div className="d-flex justify-content-between my-4 my-md-5 align-items-center">
<h3 className="m-0">{intl.formatMessage(messages.contentPermissions)}</h3>
</div>
<PageGrid pages={permissonPages} />
</>
) : ''
}

<Switch>
<PageRoute
path={[
Expand All @@ -71,6 +88,15 @@ const PagesAndResources = ({ courseId, intl }) => {
>
<DiscussionsSettings courseId={courseId} />
</PageRoute>

<PageRoute
path={[
`${path}/xpert-unit-summary/settings`,
]}
>
<XpertUnitSummarySettings courseId={courseId} />
</PageRoute>

<PageRoute path={`${path}/:appId/settings`}>
{
({ match, history }) => {
Expand Down
4 changes: 4 additions & 0 deletions src/pages-and-resources/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const messages = defineMessages({
id: 'course-authoring.badge.enabled',
defaultMessage: 'Enabled',
},
contentPermissions: {
id: 'course-authoring.pages-resources.content-permissions.heading',
defaultMessage: 'Content permissions',
},
});

export default messages;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { history } from '@edx/frontend-platform';
import { useDispatch } from 'react-redux';

import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';

import SettingsModal from './settings-modal/SettingsModal';
import messages from './messages';

import { fetchXpertSettings } from './data/thunks';

const XpertUnitSummarySettings = ({ intl }) => {
const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchXpertSettings(courseId));
}, [courseId]);

const handleClose = useCallback(() => {
history.push(pagesAndResourcesPath);
}, [pagesAndResourcesPath]);

return (
<SettingsModal
appId="xpert-unit-summary"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableXpertUnitSummaryHelp)}
enableAppLabel={intl.formatMessage(messages.enableXpertUnitSummaryLabel)}
learnMoreText={intl.formatMessage(messages.enableXpertUnitSummaryLink)}
onClose={handleClose}
/>
);
};

XpertUnitSummarySettings.propTypes = {
intl: intlShape.isRequired,
};

export default injectIntl(XpertUnitSummarySettings);
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import ReactDOM from 'react-dom';
import React from 'react';
import { Switch } from 'react-router';
import {
getConfig, history, initializeMockApp, setConfig,
} from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageRoute } from '@edx/frontend-platform/react';
import {
queryByTestId, render, waitFor, getByText, fireEvent,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
import { XpertUnitSummarySettings } from './index';
import initializeStore from '../../store';
import * as API from './data/api';
import * as Thunks from './data/thunks';
import { executeThunk } from '../../utils';

const courseId = 'course-v1:edX+TestX+Test_Course';
let axiosMock;
let store;
let container;

// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = jest.fn(node => node);

function renderComponent() {
const wrapper = render(
<AppProvider store={store}>
<PagesAndResourcesProvider courseId={courseId}>
<Switch>
<PageRoute
path={[
'/xpert-unit-summary/settings',
]}
>
<XpertUnitSummarySettings courseId={courseId} />
</PageRoute>

<PageRoute
path={[
'/',
]}
>
<div />
</PageRoute>
</Switch>
</PagesAndResourcesProvider>
</AppProvider>,
);
container = wrapper.container;
}

function generateCourseLevelAPIRepsonse({
success, enabled,
}) {
return {
response: {
success, enabled,
},
};
}

describe('XpertUnitSummarySettings', () => {
beforeEach(() => {
setConfig({
...getConfig(),
BASE_URL: 'http://test.edx.org',
LMS_BASE_URL: 'http://lmstest.edx.org',
CMS_BASE_URL: 'http://cmstest.edx.org',
LOGIN_URL: 'http://support.edx.org/login',
LOGOUT_URL: 'http://support.edx.org/logout',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://support.edx.org/access_token',
ACCESS_TOKEN_COOKIE_NAME: 'cookie',
CSRF_TOKEN_API_PATH: '/',
SUPPORT_URL: 'http://support.edx.org',
});

initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});

store = initializeStore({
models: {
courseDetails: {
[courseId]: {
start: Date(),
},
},
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());

// Go back to settings route
history.push('/xpert-unit-summary/settings');
});

describe('with successful network connections', () => {
beforeEach(() => {
axiosMock.onGet(API.getXpertSettingsUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
enabled: true,
}));

renderComponent();
});

test('Shows enabled if enabled from backend', async () => {
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
});

test('Does not show enabled if disabled from backend', async () => {
axiosMock.onGet(API.getXpertSettingsUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
enabled: false,
}));

renderComponent();
await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).not.toBeTruthy();
expect(queryByTestId(container, 'enable-badge')).not.toBeTruthy();
});
});

describe('first time course configuration', () => {
beforeEach(() => {
axiosMock.onGet(API.getXpertSettingsUrl(courseId))
.reply(400, generateCourseLevelAPIRepsonse({
success: false,
enabled: false,
}));

renderComponent();
});

test('Does not show as enabled if configuation does not exist', async () => {
await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).not.toBeTruthy();
expect(queryByTestId(container, 'enable-badge')).not.toBeTruthy();
});
});

describe('saving configuration changes', () => {
beforeEach(() => {
axiosMock.onPost(API.getXpertSettingsUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
enabled: true,
}));

renderComponent();
});

test('Saving configuration changes', async () => {
jest.spyOn(API, 'postXpertSettings');

await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
fireEvent.click(getByText(container, 'Save'));
await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).not.toBeTruthy());
expect(API.postXpertSettings).toBeCalled();
});
});

describe('testing configurable gating', () => {
beforeEach(async () => {
axiosMock.onGet(API.getXpertConfigurationStatusUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
enabled: true,
}));
jest.spyOn(API, 'getXpertPluginConfigurable');
await executeThunk(Thunks.fetchXpertPluginConfigurable(courseId), store.dispatch);
renderComponent();
});

test('getting Xpert Plugin configurable status', () => {
expect(API.getXpertPluginConfigurable).toBeCalled();
});
});
});
13 changes: 13 additions & 0 deletions src/pages-and-resources/xpert-unit-summary/appInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
id: 'xpert-unit-summary',
enabled: false,
name: 'Xpert unit summaries',
description: 'Harness ChatGPT for quick, focused summaries of text and video content.',
allowedOperations: {
enable: true,
configure: true,
},
documentationLinks: {
learnMoreConfiguration: '',
},
};
33 changes: 33 additions & 0 deletions src/pages-and-resources/xpert-unit-summary/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

export function getXpertSettingsUrl(courseId) {
return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}`;
}

export function getXpertConfigurationStatusUrl(courseId) {
return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}/configurable`;
}

export async function getXpertSettings(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getXpertSettingsUrl(courseId));

return data;
}

export async function postXpertSettings(courseId, state) {
const { data } = await getAuthenticatedHttpClient()
.post(getXpertSettingsUrl(courseId), {
enabled: state.enabled,
});

return data;
}

export async function getXpertPluginConfigurable(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getXpertConfigurationStatusUrl(courseId));

return data;
}
Loading