From 6abb49d12505269063bd5c2e134ca4d9389b3cd0 Mon Sep 17 00:00:00 2001
From: Troy Sankey
Date: Fri, 18 Oct 2024 09:45:14 -0700
Subject: [PATCH 01/21] temp: hide restricted runs before we're ready to show
them
ENT-9505
---
src/components/learner-credit-management/data/utils.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js
index ba7272086..38d47c3ff 100644
--- a/src/components/learner-credit-management/data/utils.js
+++ b/src/components/learner-credit-management/data/utils.js
@@ -698,7 +698,7 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime,
}));
const assignableCourseRunsFilter = ({
- enrollBy, enrollStart, start, hasEnrollBy, hasEnrollStart, isActive, isLateEnrollmentEligible,
+ enrollBy, enrollStart, start, hasEnrollBy, hasEnrollStart, isActive, isLateEnrollmentEligible, restrictionType,
}) => {
const isEnrollByDateValid = isEnrollByDateWithinThreshold({
hasEnrollBy,
@@ -727,6 +727,14 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime,
// to do is make sure the run itself is generally eligible for late enrollment
return isLateEnrollmentEligible;
}
+ // ENT-9359 (epic for Custom Presentations/Restricted Runs):
+ // Temporarily hide all restricted runs unconditionally on the run assignment
+ // dropdown during implementation of the overall feature. ENT-9411 is most likely
+ // the ticket to replace this code with something to actually show restricted
+ // runs conditionally.
+ if (restrictionType) {
+ return false;
+ }
// General courseware filter
return isActive;
};
From 888fb6b2f0c50558b8640e572a9072eb2a5c8b7e Mon Sep 17 00:00:00 2001
From: Alexander Dusenbery
Date: Tue, 22 Oct 2024 13:49:25 -0400
Subject: [PATCH 02/21] temp: move restrictionType check prior to check for
late redemption
---
.../data/tests/utils.test.js | 39 +++++++++++++++++++
.../learner-credit-management/data/utils.js | 14 +++----
2 files changed, 46 insertions(+), 7 deletions(-)
diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js
index bcfb62022..9e0601403 100644
--- a/src/components/learner-credit-management/data/tests/utils.test.js
+++ b/src/components/learner-credit-management/data/tests/utils.test.js
@@ -1,6 +1,7 @@
import { createIntl } from '@edx/frontend-platform/i18n';
import dayjs from 'dayjs';
import {
+ getAssignableCourseRuns,
getBudgetStatus,
getTranslatedBudgetStatus,
getTranslatedBudgetTerm,
@@ -343,3 +344,41 @@ describe('startAndEnrollBySortLogic', () => {
expect(sortedDates).toEqual(sampleData.sort((a, b) => a.expectedOrder - b.expectedOrder));
});
});
+
+describe('getAssignableCourseRuns', () => {
+ it('includes a late, non-restricted course run when late-redemption eligible', () => {
+ const courseRuns = [
+ {
+ key: 'the-course-run',
+ enrollBy: dayjs().subtract(1, 'day'),
+ hasEnrollBy: true,
+ upgradeDeadline: dayjs().add(1, 'day'),
+ start: dayjs().subtract(1, 'day'),
+ isActive: true,
+ },
+ ];
+ const subsidyExpirationDatetime = dayjs().add(100, 'day');
+ const isLateRedemptionAllowed = true;
+
+ const result = getAssignableCourseRuns({ courseRuns, subsidyExpirationDatetime, isLateRedemptionAllowed });
+ expect(result.length).toEqual(1);
+ expect(result[0].key).toEqual('the-course-run');
+ });
+ it('returns an empty list given only a restricted run , even when late-redemption eligible', () => {
+ const courseRuns = [
+ {
+ enrollBy: dayjs().subtract(1, 'day'),
+ hasEnrollBy: true,
+ restrictionType: 'b2b-enterprise',
+ upgradeDeadline: dayjs().subtract(1, 'day'),
+ start: dayjs().subtract(1, 'day'),
+ isActive: true,
+ },
+ ];
+ const subsidyExpirationDatetime = dayjs().add(100, 'day');
+ const isLateRedemptionAllowed = true;
+
+ const result = getAssignableCourseRuns({ courseRuns, subsidyExpirationDatetime, isLateRedemptionAllowed });
+ expect(result).toEqual([]);
+ });
+});
diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js
index 38d47c3ff..c8a04981e 100644
--- a/src/components/learner-credit-management/data/utils.js
+++ b/src/components/learner-credit-management/data/utils.js
@@ -720,13 +720,6 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime,
// the current date and subsidy expiration date have failed.
return false;
}
- if (hasEnrollBy && isLateRedemptionAllowed && isDateBeforeToday(enrollBy)) {
- // Special case: late enrollment has been enabled by ECS for this budget, and
- // isEligibleForEnrollment already succeeded, so we know that late enrollment
- // would be happy given enrollment deadline of the course. Now all we need
- // to do is make sure the run itself is generally eligible for late enrollment
- return isLateEnrollmentEligible;
- }
// ENT-9359 (epic for Custom Presentations/Restricted Runs):
// Temporarily hide all restricted runs unconditionally on the run assignment
// dropdown during implementation of the overall feature. ENT-9411 is most likely
@@ -735,6 +728,13 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime,
if (restrictionType) {
return false;
}
+ if (hasEnrollBy && isLateRedemptionAllowed && isDateBeforeToday(enrollBy)) {
+ // Special case: late enrollment has been enabled by ECS for this budget, and
+ // isEligibleForEnrollment already succeeded, so we know that late enrollment
+ // would be happy given enrollment deadline of the course. Now all we need
+ // to do is make sure the run itself is generally eligible for late enrollment
+ return isLateEnrollmentEligible;
+ }
// General courseware filter
return isActive;
};
From 8a30273fd59fbbbac5a18ac1140527cb5439b279 Mon Sep 17 00:00:00 2001
From: muhammad-ammar
Date: Thu, 17 Oct 2024 11:55:39 +0500
Subject: [PATCH 03/21] feat: lpr budgets filtering
---
src/components/Admin/Admin.test.jsx | 31 +
src/components/Admin/AdminSearchForm.jsx | 64 +-
src/components/Admin/AdminSearchForm.test.jsx | 33 +
.../Admin/__snapshots__/Admin.test.jsx.snap | 2377 +++++++++++++++--
src/components/Admin/index.jsx | 12 +-
src/containers/AdminPage/AdminPage.test.jsx | 4 +
src/containers/AdminPage/index.jsx | 9 +
src/data/actions/enterpriseBudgets.js | 41 +
src/data/constants/enterpriseBudgets.js | 11 +
src/data/reducers/enterpriseBudgets.js | 47 +
src/data/reducers/enterpriseBudgets.test.js | 80 +
src/data/reducers/index.js | 2 +
src/data/services/EnterpriseDataApiService.js | 6 +
src/utils.js | 3 +
14 files changed, 2555 insertions(+), 165 deletions(-)
create mode 100644 src/data/actions/enterpriseBudgets.js
create mode 100644 src/data/constants/enterpriseBudgets.js
create mode 100644 src/data/reducers/enterpriseBudgets.js
create mode 100644 src/data/reducers/enterpriseBudgets.test.js
diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx
index c0a1105c7..0cd7685cf 100644
--- a/src/components/Admin/Admin.test.jsx
+++ b/src/components/Admin/Admin.test.jsx
@@ -54,6 +54,10 @@ const store = mockStore({
loading: null,
insights: null,
},
+ enterpriseBudgets: {
+ loading: null,
+ budgets: null,
+ },
});
const AdminWrapper = props => (
@@ -78,8 +82,14 @@ const AdminWrapper = props => (
pathname: '/',
}}
{...props}
+ budgets={[{
+ subsidy_access_policy_uuid: '8d6503dd-e40d-42b8-442b-37dd4c5450e3',
+ subsidy_access_policy_display_name: 'Everything',
+ }]}
fetchDashboardInsights={() => {}}
clearDashboardInsights={() => {}}
+ fetchEnterpriseBudgets={() => {}}
+ clearEnterpriseBudgets={() => {}}
/>
@@ -97,6 +107,7 @@ describe('', () => {
lastUpdatedDate: '2018-07-31T23:14:35Z',
numberOfUsers: 3,
insights: null,
+ budgets: [],
};
describe('renders correctly', () => {
@@ -370,6 +381,26 @@ describe('', () => {
expect(tree).toMatchSnapshot();
});
});
+
+ describe('with enterprise budgets data', () => {
+ it('renders budgets correctly', () => {
+ const budgetUUID = '8d6503dd-e40d-42b8-442b-37dd4c5450e3';
+ const budgets = [{
+ subsidy_access_policy_uuid: budgetUUID,
+ subsidy_access_policy_display_name: 'Everything',
+ }];
+ const tree = renderer
+ .create((
+
+ ))
+ .toJSON();
+
+ expect(tree).toMatchSnapshot();
+ });
+ });
});
describe('handle changes to enterpriseId prop', () => {
diff --git a/src/components/Admin/AdminSearchForm.jsx b/src/components/Admin/AdminSearchForm.jsx
index a2624cef8..e93626f16 100644
--- a/src/components/Admin/AdminSearchForm.jsx
+++ b/src/components/Admin/AdminSearchForm.jsx
@@ -1,6 +1,7 @@
/* eslint-disable camelcase */
import React from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import { Form } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
@@ -14,17 +15,22 @@ import { withLocation, withNavigate } from '../../hoc';
class AdminSearchForm extends React.Component {
componentDidUpdate(prevProps) {
- const { searchParams: { searchQuery, searchCourseQuery, searchDateQuery } } = this.props;
+ const {
+ searchParams: {
+ searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery,
+ },
+ } = this.props;
const {
searchParams: {
searchQuery: prevSearchQuery,
searchCourseQuery: prevSearchCourseQuery,
searchDateQuery: prevSearchDateQuery,
+ searchBudgetQuery: prevSearchBudgetQuery,
},
} = prevProps;
if (searchQuery !== prevSearchQuery || searchCourseQuery !== prevSearchCourseQuery
- || searchDateQuery !== prevSearchDateQuery) {
+ || searchDateQuery !== prevSearchDateQuery || searchBudgetQuery !== prevSearchBudgetQuery) {
this.handleSearch();
}
}
@@ -45,14 +51,27 @@ class AdminSearchForm extends React.Component {
updateUrl(navigate, location.pathname, updateParams);
}
+ onBudgetSelect(event) {
+ const { navigate, location } = this.props;
+ const updateParams = {
+ budget_uuid: event.target.value,
+ page: 1,
+ };
+ updateUrl(navigate, location.pathname, updateParams);
+ }
+
render() {
const {
intl,
tableData,
- searchParams: { searchCourseQuery, searchDateQuery, searchQuery },
+ budgets,
+ searchParams: {
+ searchCourseQuery, searchDateQuery, searchQuery, searchBudgetQuery,
+ },
} = this.props;
const courseTitles = Array.from(new Set(tableData.map(en => en.course_title).sort()));
const courseDates = Array.from(new Set(tableData.map(en => en.course_start_date).sort().reverse()));
+ const columnWidth = budgets?.length ? 'col-md-3' : 'col-md-6';
return (
@@ -151,7 +170,7 @@ class AdminSearchForm extends React.Component {
-
+
+ {budgets?.length && (
+
+
+
+
+
+ this.onBudgetSelect(e)}
+ >
+
+ {budgets.map(budget => (
+
+ ))}
+
+
+
+ )}
@@ -193,8 +247,10 @@ AdminSearchForm.propTypes = {
searchQuery: PropTypes.string,
searchCourseQuery: PropTypes.string,
searchDateQuery: PropTypes.string,
+ searchBudgetQuery: PropTypes.string,
}).isRequired,
tableData: PropTypes.arrayOf(PropTypes.shape({})),
+ budgets: PropTypes.arrayOf(PropTypes.shape({})),
navigate: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string,
diff --git a/src/components/Admin/AdminSearchForm.test.jsx b/src/components/Admin/AdminSearchForm.test.jsx
index 7a0d944eb..dada76b68 100644
--- a/src/components/Admin/AdminSearchForm.test.jsx
+++ b/src/components/Admin/AdminSearchForm.test.jsx
@@ -5,6 +5,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import AdminSearchForm from './AdminSearchForm';
import SearchBar from '../SearchBar';
+import { updateUrl } from '../../utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -12,6 +13,10 @@ jest.mock('react-router-dom', () => ({
useNavigate: jest.fn(),
}));
+jest.mock('../../utils', () => ({
+ updateUrl: jest.fn(),
+}));
+
const DEFAULT_PROPS = {
searchEnrollmentsList: () => {},
searchParams: {},
@@ -48,4 +53,32 @@ describe('', () => {
expect(spy).toHaveBeenCalledTimes(1);
});
});
+
+ it('select the correct budget', () => {
+ const budgetUUID = '8d6503dd-e40d-42b8-442b-37dd4c5450e3';
+ const budgets = [{
+ subsidy_access_policy_uuid: budgetUUID,
+ subsidy_access_policy_display_name: 'Everything',
+ }];
+ const props = {
+ ...DEFAULT_PROPS,
+ budgets,
+ location: { pathname: '/admin/learners' },
+ };
+ const wrapper = mount(
+ ,
+ );
+ const selectElement = wrapper.find('.budgets-dropdown select');
+
+ selectElement.simulate('change', { target: { value: budgetUUID } });
+ expect(updateUrl).toHaveBeenCalled();
+ expect(updateUrl).toHaveBeenCalledWith(
+ undefined,
+ '/admin/learners',
+ {
+ budget_uuid: budgetUUID,
+ page: 1,
+ },
+ );
+ });
});
diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap
index 234d84211..5b2458ef5 100644
--- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap
+++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap
@@ -1665,7 +1665,7 @@ exports[` renders correctly with dashboard analytics data renders # cou
>
@@ -1674,7 +1674,7 @@ exports[` renders correctly with dashboard analytics data renders # cou
>
renders correctly with dashboard analytics data renders # cou
>
- {formatter.format(data?.enrolls || 0)}
+ {formatNumber(data?.enrolls || 0)}
@@ -45,7 +48,7 @@ const Stats = ({
description="Title for the distinct courses stat."
/>
-
{formatter.format(data?.courses || 0)}
+
{formatNumber(data?.courses || 0)}
@@ -55,7 +58,7 @@ const Stats = ({
description="Title for the daily sessions stat."
/>
-
{formatter.format(data?.sessions || 0)}
+
{formatNumber(data?.sessions || 0)}
@@ -65,7 +68,7 @@ const Stats = ({
description="Title for the learning hours stat."
/>
-
{formatter.format(data?.hours || 0)}
+
{formatNumber(data?.hours || 0)}
@@ -75,7 +78,7 @@ const Stats = ({
description="Title for the completions stat."
/>
-
{formatter.format(data?.completions || 0)}
+
{formatNumber(data?.completions || 0)}
diff --git a/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx b/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
index 2935d134f..e91088060 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
@@ -66,8 +66,9 @@ const AnalyticsTable = ({
)}
diff --git a/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx b/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx
index 4c3b8fc03..664fd2bb9 100644
--- a/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx
+++ b/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx
@@ -23,7 +23,7 @@ describe('Stats', () => {
expect(wrapper.find('.title-distinct-courses').text()).toEqual('Distinct Courses');
expect(wrapper.find('.value-distinct-courses').text()).toEqual('365');
expect(wrapper.find('.title-daily-sessions').text()).toEqual('Daily Sessions');
- expect(wrapper.find('.value-daily-sessions').text()).toEqual('1.89K');
+ expect(wrapper.find('.value-daily-sessions').text()).toEqual('1892');
expect(wrapper.find('.title-learning-hours').text()).toEqual('Learning Hours');
expect(wrapper.find('.value-learning-hours').text()).toEqual('25.35M');
expect(wrapper.find('.title-completions').text()).toEqual('Completions');
From ba8fc5654e6492d7b7b4407f234a4ea506313e6f Mon Sep 17 00:00:00 2001
From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com>
Date: Tue, 12 Nov 2024 08:05:41 -0800
Subject: [PATCH 07/21] feat: add enterprise users datatable (#1341)
* feat: add enterprise users datatable
---
.../CreateGroupModalContent.jsx | 24 ++
.../tests/CreateGroupModal.test.jsx | 102 +++++++-
.../hooks/useEnterpriseLearnersTableData.js | 79 ++++++
.../EnterpriseCustomerUserDatatable.jsx | 230 ++++++++++++++++++
.../invite-modal/InviteModalContent.jsx | 7 +-
.../invite-modal/InviteSummaryCount.jsx | 2 +-
src/data/services/LmsApiService.js | 13 +-
7 files changed, 450 insertions(+), 7 deletions(-)
create mode 100644 src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js
create mode 100644 src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx
diff --git a/src/components/PeopleManagement/CreateGroupModalContent.jsx b/src/components/PeopleManagement/CreateGroupModalContent.jsx
index 739f9cf62..f871254d1 100644
--- a/src/components/PeopleManagement/CreateGroupModalContent.jsx
+++ b/src/components/PeopleManagement/CreateGroupModalContent.jsx
@@ -13,6 +13,7 @@ import InviteSummaryCount from '../learner-credit-management/invite-modal/Invite
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
import { MAX_LENGTH_GROUP_NAME } from './constants';
+import EnterpriseCustomerUserDatatable from '../learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable';
const CreateGroupModalContent = ({
onEmailAddressesChange,
@@ -43,6 +44,24 @@ const CreateGroupModalContent = ({
onSetGroupName(e.target.value);
}, [onSetGroupName]);
+ const handleAddMembersBulkAction = useCallback((value) => {
+ if (!value) {
+ setLearnerEmails([]);
+ onEmailAddressesChange([]);
+ return;
+ }
+ setLearnerEmails(prev => [...prev, ...value]);
+ }, [onEmailAddressesChange]);
+
+ const handleRemoveMembersBulkAction = useCallback((value) => {
+ if (!value) {
+ setLearnerEmails([]);
+ onEmailAddressesChange([]);
+ return;
+ }
+ setLearnerEmails(prev => prev.filter((el) => !value.includes(el)));
+ }, [onEmailAddressesChange]);
+
const handleEmailAddressesChanged = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
@@ -123,6 +142,11 @@ const CreateGroupModalContent = ({
+
);
};
diff --git a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
index 06152da56..ba55f11ad 100644
--- a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
+++ b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
@@ -13,12 +13,21 @@ import { queryClient } from '../../test/testUtils';
import LmsApiService from '../../../data/services/LmsApiService';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from '../../learner-credit-management/cards/data';
import CreateGroupModal from '../CreateGroupModal';
+import {
+ useEnterpriseLearnersTableData,
+ useGetAllEnterpriseLearnerEmails,
+} from '../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData';
jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(),
}));
jest.mock('../../../data/services/LmsApiService');
+jest.mock('../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData', () => ({
+ ...jest.requireActual('../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData'),
+ useEnterpriseLearnersTableData: jest.fn(),
+ useGetAllEnterpriseLearnerEmails: jest.fn(),
+}));
const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
@@ -43,6 +52,45 @@ const defaultProps = {
enterpriseUUID: 'test-uuid',
};
+const mockTabledata = {
+ itemCount: 3,
+ pageCount: 1,
+ results: [
+ {
+ id: 1,
+ user: {
+ id: 1,
+ username: 'testuser-1',
+ firstName: '',
+ lastName: '',
+ email: 'testuser-1@2u.com',
+ dateJoined: '2023-05-09T16:18:22Z',
+ },
+ },
+ {
+ id: 2,
+ user: {
+ id: 2,
+ username: 'testuser-2',
+ firstName: '',
+ lastName: '',
+ email: 'testuser-2@2u.com',
+ dateJoined: '2023-05-09T16:18:22Z',
+ },
+ },
+ {
+ id: 3,
+ user: {
+ id: 3,
+ username: 'testuser-3',
+ firstName: '',
+ lastName: '',
+ email: 'testuser-3@2u.com',
+ dateJoined: '2023-05-09T16:18:22Z',
+ },
+ },
+ ],
+};
const CreateGroupModalWrapper = ({
initialState = initialStoreState,
}) => {
@@ -59,6 +107,18 @@ const CreateGroupModalWrapper = ({
};
describe('', () => {
+ beforeEach(() => {
+ useEnterpriseLearnersTableData.mockReturnValue({
+ isLoading: false,
+ enterpriseCustomerUserTableData: mockTabledata,
+ fetchEnterpriseLearnersData: jest.fn(),
+ });
+ useGetAllEnterpriseLearnerEmails.mockReturnValue({
+ isLoading: false,
+ fetchLearnerEmails: jest.fn(),
+ addButtonState: 'complete',
+ });
+ });
it('Modal renders as expected', async () => {
render();
expect(screen.getByText('Create a custom group of members')).toBeInTheDocument();
@@ -69,6 +129,16 @@ describe('', () => {
expect(screen.getByText('Upload a CSV file or select members to get started.')).toBeInTheDocument();
expect(screen.getByText('Create')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();
+
+ // renders datatable
+ expect(screen.getByText('Member details')).toBeInTheDocument();
+ expect(screen.getByText('Joined organization')).toBeInTheDocument();
+ expect(screen.getByText('testuser-1')).toBeInTheDocument();
+ expect(screen.getByText('testuser-1@2u.com')).toBeInTheDocument();
+ expect(screen.getByText('testuser-2')).toBeInTheDocument();
+ expect(screen.getByText('testuser-2@2u.com')).toBeInTheDocument();
+ expect(screen.getByText('testuser-3')).toBeInTheDocument();
+ expect(screen.getByText('testuser-3@2u.com')).toBeInTheDocument();
});
it('creates groups and assigns learners', async () => {
const mockCreateGroup = jest.spyOn(LmsApiService, 'createEnterpriseGroup');
@@ -93,10 +163,36 @@ describe('', () => {
userEvent.type(groupNameInput, 'test group name');
await waitFor(() => {
- expect(screen.getByText('emails.csv')).toBeInTheDocument();
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
+ expect(screen.getByText('tomhaverford@pawnee.org')).toBeInTheDocument();
+ }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
+
+ // testing interaction with adding members from the datatable
+ const membersCheckbox = screen.getAllByTitle('Toggle Row Selected');
+ userEvent.click(membersCheckbox[0]);
+ userEvent.click(membersCheckbox[1]);
+ const addMembersButton = screen.getByText('Add');
+ userEvent.click(addMembersButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('Summary (3)')).toBeInTheDocument();
+ // checking that each user appears twice, once in the datatable and once in the summary section
+ expect(screen.getAllByText('testuser-1@2u.com')).toHaveLength(2);
+ expect(screen.getAllByText('testuser-2@2u.com')).toHaveLength(2);
+ });
+
+ // testing interaction with removing members from the datatable
+ const removeMembersButton = screen.getByText('Remove');
+ userEvent.click(removeMembersButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('Summary (1)')).toBeInTheDocument();
+ expect(screen.getByText('emails.csv')).toBeInTheDocument();
expect(screen.getByText('Total members to add')).toBeInTheDocument();
expect(screen.getByText('tomhaverford@pawnee.org')).toBeInTheDocument();
+ expect(screen.getAllByText('testuser-1@2u.com')).toHaveLength(1);
+ expect(screen.getAllByText('testuser-2@2u.com')).toHaveLength(1);
+ expect(screen.getAllByText('testuser-3@2u.com')).toHaveLength(1);
const formFeedbackText = 'Maximum members at a time: 1000';
expect(screen.queryByText(formFeedbackText)).not.toBeInTheDocument();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
@@ -137,7 +233,9 @@ describe('', () => {
const createButton = screen.getByRole('button', { name: 'Create' });
userEvent.click(createButton);
await waitFor(() => {
- expect(screen.getByText('We\'re sorry. Something went wrong behind the scenes. Please try again, or reach out to customer support for help.')).toBeInTheDocument();
+ expect(screen.getByText(
+ 'We\'re sorry. Something went wrong behind the scenes. Please try again, or reach out to customer support for help.',
+ )).toBeInTheDocument();
});
});
});
diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js b/src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js
new file mode 100644
index 000000000..78fd7df4c
--- /dev/null
+++ b/src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js
@@ -0,0 +1,79 @@
+import {
+ useCallback, useMemo, useState,
+} from 'react';
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import { logError } from '@edx/frontend-platform/logging';
+import debounce from 'lodash.debounce';
+
+import LmsApiService from '../../../../data/services/LmsApiService';
+import { fetchPaginatedData } from '../../../../data/services/apiServiceUtils';
+
+export const useGetAllEnterpriseLearnerEmails = ({
+ enterpriseId,
+ onHandleAddMembersBulkAction,
+}) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [addButtonState, setAddButtonState] = useState('default');
+
+ const fetchLearnerEmails = useCallback(async () => {
+ setAddButtonState('pending');
+ try {
+ const url = `${LmsApiService.enterpriseLearnerUrl}?enterprise_customer=${enterpriseId}`;
+ const { results } = await fetchPaginatedData(url);
+ const learnerEmails = results.map(result => result?.user?.email).filter(email => email !== undefined);
+ onHandleAddMembersBulkAction(learnerEmails);
+ } catch (error) {
+ logError(error);
+ setAddButtonState('error');
+ } finally {
+ setIsLoading(false);
+ setAddButtonState('complete');
+ }
+ }, [enterpriseId, onHandleAddMembersBulkAction]);
+
+ return {
+ isLoading,
+ fetchLearnerEmails,
+ addButtonState,
+ };
+};
+
+export const useEnterpriseLearnersTableData = (enterpriseId) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [enterpriseCustomerUserTableData, setEnterpriseCustomerUserTableData] = useState({
+ itemCount: 0,
+ pageCount: 0,
+ results: [],
+ });
+ const fetchEnterpriseLearnersData = useCallback(async (args) => {
+ try {
+ setIsLoading(true);
+ const options = {
+ enterprise_customer: enterpriseId,
+ };
+ options.page = args.pageIndex + 1;
+ const response = await LmsApiService.fetchEnterpriseLearners(options);
+ const { data } = camelCaseObject(response);
+ setEnterpriseCustomerUserTableData({
+ itemCount: data.count,
+ pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
+ results: data.results,
+ });
+ } catch (error) {
+ logError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [enterpriseId, setEnterpriseCustomerUserTableData]);
+
+ const debouncedFetchEnterpriseLearnersData = useMemo(
+ () => debounce(fetchEnterpriseLearnersData, 300),
+ [fetchEnterpriseLearnersData],
+ );
+
+ return {
+ isLoading,
+ enterpriseCustomerUserTableData,
+ fetchEnterpriseLearnersData: debouncedFetchEnterpriseLearnersData,
+ };
+};
diff --git a/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx b/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx
new file mode 100644
index 000000000..eeee91067
--- /dev/null
+++ b/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx
@@ -0,0 +1,230 @@
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import {
+ Button,
+ DataTable,
+ Stack,
+ StatefulButton,
+ TextFilter,
+} from '@openedx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { useGetAllEnterpriseLearnerEmails, useEnterpriseLearnersTableData } from '../data/hooks/useEnterpriseLearnersTableData';
+import { formatTimestamp } from '../../../utils';
+import { DEFAULT_PAGE, MEMBERS_TABLE_PAGE_SIZE } from '../data';
+
+const getSelectedEmailsByRow = (selectedFlatRows) => {
+ const emails = [];
+ Object.keys(selectedFlatRows).forEach(key => {
+ const { original } = selectedFlatRows[key];
+ if (original.user !== null) {
+ emails.push(original.user.email);
+ }
+ });
+ return emails;
+};
+
+const MemberDetailsCell = ({ row }) => (
+
+
+ {row.original?.user?.username}
+
+
+ {row.original?.user?.email}
+
+
+);
+
+const MemberJoinedDateCell = ({ row }) => (
+
+ {formatTimestamp({ timestamp: row.original.created, format: 'MMM DD, YYYY' })}
+
+);
+
+const AddMembersBulkAction = ({
+ isEntireTableSelected,
+ selectedFlatRows,
+ onHandleAddMembersBulkAction,
+ enterpriseId,
+}) => {
+ const intl = useIntl();
+ const { fetchLearnerEmails, addButtonState } = useGetAllEnterpriseLearnerEmails({
+ enterpriseId,
+ isEntireTableSelected,
+ onHandleAddMembersBulkAction,
+ });
+ const handleOnClick = () => {
+ if (isEntireTableSelected) {
+ fetchLearnerEmails();
+ return;
+ }
+ const emails = getSelectedEmailsByRow(selectedFlatRows);
+ onHandleAddMembersBulkAction(emails);
+ };
+
+ return (
+
+ );
+};
+
+const RemoveMembersBulkAction = ({
+ isEntireTableSelected,
+ selectedFlatRows,
+ onHandleRemoveMembersBulkAction,
+ learnerEmails,
+}) => {
+ const handleOnClick = async () => {
+ if (isEntireTableSelected) {
+ onHandleRemoveMembersBulkAction(learnerEmails);
+ }
+ const emails = getSelectedEmailsByRow(selectedFlatRows);
+ onHandleRemoveMembersBulkAction(emails);
+ };
+
+ return (
+
+ );
+};
+
+const selectColumn = {
+ id: 'selection',
+ Header: DataTable.ControlledSelectHeader,
+ Cell: DataTable.ControlledSelect,
+};
+
+// TO-DO: add search functionality on member details once the learner endpoint is updated
+// to support search
+const EnterpriseCustomerUserDatatable = ({
+ enterpriseId,
+ learnerEmails,
+ onHandleAddMembersBulkAction,
+ onHandleRemoveMembersBulkAction,
+}) => {
+ const {
+ isLoading,
+ enterpriseCustomerUserTableData,
+ fetchEnterpriseLearnersData,
+ } = useEnterpriseLearnersTableData(enterpriseId);
+
+ return (
+ ,
+ ,
+ ]}
+ columns={[
+ {
+ Header: 'Member details',
+ accessor: 'user.email',
+ Cell: MemberDetailsCell,
+ },
+ {
+ Header: 'Joined organization',
+ accessor: 'created',
+ Cell: MemberJoinedDateCell,
+ disableFilters: true,
+ },
+ ]}
+ initialState={{
+ pageIndex: DEFAULT_PAGE,
+ pageSize: MEMBERS_TABLE_PAGE_SIZE,
+ }}
+ data={enterpriseCustomerUserTableData.results}
+ defaultColumnValues={{ Filter: TextFilter }}
+ fetchData={fetchEnterpriseLearnersData}
+ isFilterable
+ isLoading={isLoading}
+ isPaginated
+ isSelectable
+ itemCount={enterpriseCustomerUserTableData.itemCount}
+ manualFilters
+ manualPagination
+ initialTableOptions={{
+ getRowId: row => row.id.toString(),
+ }}
+ pageCount={enterpriseCustomerUserTableData.pageCount}
+ SelectionStatusComponent={DataTable.ControlledSelectionStatus}
+ manualSelectColumn={selectColumn}
+ />
+ );
+};
+
+MemberDetailsCell.propTypes = {
+ row: PropTypes.shape({
+ original: PropTypes.shape({
+ user: PropTypes.shape({
+ email: PropTypes.string.isRequired,
+ username: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+MemberJoinedDateCell.propTypes = {
+ row: PropTypes.shape({
+ original: PropTypes.shape({
+ created: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+AddMembersBulkAction.propTypes = {
+ isEntireTableSelected: PropTypes.bool.isRequired,
+ selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
+ enterpriseId: PropTypes.string.isRequired,
+ onHandleAddMembersBulkAction: PropTypes.func.isRequired,
+};
+
+RemoveMembersBulkAction.propTypes = {
+ isEntireTableSelected: PropTypes.bool.isRequired,
+ learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
+ selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
+ onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
+};
+
+EnterpriseCustomerUserDatatable.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+ learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
+ onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
+ onHandleAddMembersBulkAction: PropTypes.func.isRequired,
+};
+
+const mapStateToProps = state => ({
+ enterpriseId: state.portalConfiguration.enterpriseId,
+});
+
+export default connect(mapStateToProps)(EnterpriseCustomerUserDatatable);
diff --git a/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx b/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
index 06d1966fa..3495e9a89 100644
--- a/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
@@ -4,7 +4,10 @@ import React, {
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import {
- Col, Container, Form, Row,
+ Col,
+ Container,
+ Form,
+ Row,
} from '@openedx/paragon';
import InviteModalSummary from './InviteModalSummary';
@@ -58,7 +61,7 @@ const InviteModalContent = ({
return;
}
const emails = value.split('\n').map((email) => email.trim()).filter((email) => email.length > 0);
- setLearnerEmails(emails);
+ setLearnerEmails(prev => [...prev, ...emails]);
}, [onEmailAddressesChange]);
const debouncedHandleEmailAddressesChanged = useMemo(
diff --git a/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx b/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
index cfd37251e..a87836747 100644
--- a/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Card } from '@openedx/paragon';
const InviteSummaryCount = ({ memberInviteMetadata }) => (
-
+
Total members to add
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 0fb9831dd..7fd5fc65e 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -45,6 +45,8 @@ class LmsApiService {
static enterpriseGroupListUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise_group/`;
+ static enterpriseLearnerUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-learner/`;
+
static createEnterpriseGroup(options) {
const postParams = {
name: options.groupName,
@@ -422,12 +424,11 @@ class LmsApiService {
}
static fetchEnterpriseLearnerData = async (options) => {
- const enterpriseLearnerUrl = `${configuration.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/`;
const queryParams = new URLSearchParams({
...options,
page: 1,
});
- const url = `${enterpriseLearnerUrl}?${queryParams.toString()}`;
+ const url = `${LmsApiService.enterpriseLearnerUrl}?${queryParams.toString()}`;
const response = await LmsApiService.fetchData(url);
return response;
};
@@ -475,6 +476,14 @@ class LmsApiService {
const removeLearnerEndpoint = `${LmsApiService.enterpriseGroupListUrl}${groupUuid}/remove_learners/`;
return LmsApiService.apiClient().post(removeLearnerEndpoint, formData);
};
+
+ static fetchEnterpriseLearners = async (options) => {
+ const queryParams = new URLSearchParams({
+ ...options,
+ });
+ const url = `${LmsApiService.enterpriseLearnerUrl}?${queryParams.toString()}`;
+ return LmsApiService.apiClient().get(url);
+ };
}
export default LmsApiService;
From 149f4e17eb559db2a9a839c6bcd33a19194a1415 Mon Sep 17 00:00:00 2001
From: jajjibhai008
Date: Tue, 12 Nov 2024 19:04:53 +0500
Subject: [PATCH 08/21] fix: resolve some issue on analytics v2
---
src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx | 7 ++++++-
src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
index a4d6af084..9f6c649c3 100644
--- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
+++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
@@ -31,6 +31,10 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
endDate,
});
const currentDate = new Date().toISOString().split('T')[0];
+ const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: 'long', day: 'numeric' };
+ return new Date(dateString).toLocaleDateString(undefined, options);
+ };
return (
<>
@@ -43,7 +47,7 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
id="advance.analytics.data.refresh.msg"
defaultMessage="Data updated on {date}"
description="Data refresh message"
- values={{ date: data?.lastUpdatedAt || currentDate }}
+ values={{ date: formatDate(data?.lastUpdatedAt || currentDate) }}
/>
@@ -191,6 +195,7 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
{
setActiveTab(tab);
diff --git a/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx b/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
index e91088060..68adf1b46 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
@@ -62,7 +62,7 @@ const AnalyticsTable = ({
title={tableTitle}
subtitle={tableSubtitle}
DownloadCSVComponent={(
-
+
Date: Wed, 13 Nov 2024 15:52:01 +0500
Subject: [PATCH 09/21] fix: analytics v2
---
src/components/AdvanceAnalyticsV2/charts/BarChart.jsx | 2 +-
src/components/AdvanceAnalyticsV2/charts/LineChart.jsx | 1 -
src/components/AdvanceAnalyticsV2/tabs/Completions.jsx | 2 +-
src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx | 2 +-
src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx | 2 +-
5 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/components/AdvanceAnalyticsV2/charts/BarChart.jsx b/src/components/AdvanceAnalyticsV2/charts/BarChart.jsx
index d9bf6d173..e620b56d2 100644
--- a/src/components/AdvanceAnalyticsV2/charts/BarChart.jsx
+++ b/src/components/AdvanceAnalyticsV2/charts/BarChart.jsx
@@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
const BarChart = ({
data, xKey, yKey, colorKey, colorMap, hovertemplate, xAxisTitle, yAxisTitle,
}) => {
- const categories = Object.keys(colorMap);
+ const categories = Object.keys(colorMap).sort();
const traces = useMemo(() => categories.map(category => {
const filteredData = data.filter(item => item[colorKey] === category);
diff --git a/src/components/AdvanceAnalyticsV2/charts/LineChart.jsx b/src/components/AdvanceAnalyticsV2/charts/LineChart.jsx
index be4dd17bd..0e893865c 100644
--- a/src/components/AdvanceAnalyticsV2/charts/LineChart.jsx
+++ b/src/components/AdvanceAnalyticsV2/charts/LineChart.jsx
@@ -41,7 +41,6 @@ const LineChart = ({
},
xaxis: { title: xAxisTitle },
yaxis: { title: yAxisTitle },
- dragmode: false,
autosize: true,
};
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx b/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx
index 9b470ce77..9e5d35d54 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx
@@ -138,7 +138,7 @@ const Completions = ({
chartType="BarChart"
chartProps={{
data: data?.topCoursesByCompletions,
- xKey: 'courseTitle',
+ xKey: 'courseKey',
yKey: 'completionCount',
colorKey: 'enrollType',
colorMap: chartColorMap,
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
index 5f5396ffa..5bf4085d2 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
@@ -137,7 +137,7 @@ const Engagements = ({
chartType="BarChart"
chartProps={{
data: data?.topCoursesByEngagement,
- xKey: 'courseTitle',
+ xKey: 'courseKey',
yKey: 'learningTimeHours',
colorKey: 'enrollType',
colorMap: chartColorMap,
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
index 5d7940d2e..357d083d2 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
@@ -138,7 +138,7 @@ const Enrollments = ({
chartType="BarChart"
chartProps={{
data: data?.topCoursesByEnrollments,
- xKey: 'courseTitle',
+ xKey: 'courseKey',
yKey: 'enrollmentCount',
colorKey: 'enrollType',
colorMap: chartColorMap,
From 8a65672f8cc64c5c3ac61ceb39a9ffa78bda4cdf Mon Sep 17 00:00:00 2001
From: jajjibhai008
Date: Thu, 14 Nov 2024 16:41:27 +0500
Subject: [PATCH 10/21] fix: csv name issue in analytics v2
---
.../AdvanceAnalyticsV2/AnalyticsV2Page.jsx | 25 +++++++++----------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
index 9f6c649c3..1401e6e9a 100644
--- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
+++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
@@ -22,8 +22,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
const [activeTab, setActiveTab] = useState('enrollments');
const [granularity, setGranularity] = useState(GRANULARITY.WEEKLY);
const [calculation, setCalculation] = useState('Total');
- const [startDate, setStartDate] = useState('');
- const [endDate, setEndDate] = useState('');
+ const [startDate, setStartDate] = useState();
+ const [endDate, setEndDate] = useState();
const intl = useIntl();
const { isFetching, isError, data } = useEnterpriseAnalyticsAggregatesData({
enterpriseCustomerUUID: enterpriseId,
@@ -195,7 +195,6 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
{
setActiveTab(tab);
@@ -210,8 +209,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
})}
>
{
})}
>
{
})}
>
{
})}
>
@@ -272,8 +271,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
})}
>
From 8f6b45ca0eed66cff84161419942f172d9df2a1d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Nov 2024 16:05:38 -0500
Subject: [PATCH 11/21] chore(deps): bump http-proxy-middleware from 2.0.6 to
2.0.7 (#1342)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hamzah Ullah
---
package-lock.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 3816075a0..ded5dc78b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12427,8 +12427,9 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "2.0.6",
- "license": "MIT",
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
+ "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
From 7eb28c8b3ca11c2feb4f05d03f4f3ca86f7b5784 Mon Sep 17 00:00:00 2001
From: Hamzah Ullah
Date: Mon, 18 Nov 2024 16:45:51 -0500
Subject: [PATCH 12/21] fix: specify parent class for css changes (#1353)
---
src/components/PeopleManagement/GroupCardGrid.jsx | 1 +
src/components/PeopleManagement/_PeopleManagement.scss | 8 +++++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/components/PeopleManagement/GroupCardGrid.jsx b/src/components/PeopleManagement/GroupCardGrid.jsx
index fd65adf21..8c1cde76e 100644
--- a/src/components/PeopleManagement/GroupCardGrid.jsx
+++ b/src/components/PeopleManagement/GroupCardGrid.jsx
@@ -33,6 +33,7 @@ const GroupCardGrid = ({ groups }) => {
Date: Mon, 18 Nov 2024 17:20:40 -0500
Subject: [PATCH 13/21] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6
(#1354)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)
---
updated-dependencies:
- dependency-name: cross-spawn
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hamzah Ullah
---
package-lock.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ded5dc78b..d3cb9369b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8680,8 +8680,9 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "license": "MIT",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
From 0561675dfa79d3b33dfe1a02c512cf28ca0b062d Mon Sep 17 00:00:00 2001
From: Alexander Dusenbery
Date: Mon, 18 Nov 2024 11:23:32 -0500
Subject: [PATCH 14/21] feat: no 14-day threshold for allowed refundable time
---
src/components/learner-credit-management/data/constants.js | 3 ---
src/components/learner-credit-management/data/utils.js | 3 +--
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js
index ec15d598c..abc7e43c4 100644
--- a/src/components/learner-credit-management/data/constants.js
+++ b/src/components/learner-credit-management/data/constants.js
@@ -102,9 +102,6 @@ export const ENROLL_BY_DATE_DAYS_THRESHOLD = 10;
// Allocation assignment expiration dropoff threshold
export const DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION = 90;
-// Maximum days allowed from enrollment for a refund on assignments related to policies
-export const MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS = 14;
-
// When the start date is before this number of days before today, display the alternate start date (fixed to today).
export const START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14;
diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js
index c8a04981e..52a750c05 100644
--- a/src/components/learner-credit-management/data/utils.js
+++ b/src/components/learner-credit-management/data/utils.js
@@ -14,7 +14,6 @@ import {
DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION,
LATE_ENROLLMENTS_BUFFER_DAYS,
LOW_REMAINING_BALANCE_PERCENT_THRESHOLD,
- MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS,
NO_BALANCE_REMAINING_DOLLAR_THRESHOLD,
START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS,
} from './constants';
@@ -566,7 +565,7 @@ export const isLmsBudget = (
*/
export const isDateBeforeToday = date => dayjs(date).isBefore(dayjs());
-const subsidyExpirationRefundCutoffDate = ({ subsidyExpirationDatetime }) => dayjs(subsidyExpirationDatetime).subtract(MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS, 'days').toDate();
+const subsidyExpirationRefundCutoffDate = ({ subsidyExpirationDatetime }) => dayjs(subsidyExpirationDatetime).toDate();
export const isCourseSelfPaced = ({ pacingType }) => pacingType === COURSE_PACING_MAP.SELF_PACED;
From 4313f454e7e961b3041211691119cbe6201c6ec4 Mon Sep 17 00:00:00 2001
From: jajjibhai008
Date: Thu, 21 Nov 2024 15:49:13 +0500
Subject: [PATCH 15/21] feat: added dismissable warning banner on analytics v2
---
.../AdvanceAnalyticsV2/AnalyticsV2Page.jsx | 7 +++-
.../AdvanceAnalyticsV2/WarningBanner.jsx | 29 +++++++++++++++++
.../AdvanceAnalyticsV2/data/constants.js | 2 ++
.../tests/WarningBanner.test.jsx | 32 +++++++++++++++++++
4 files changed, 69 insertions(+), 1 deletion(-)
create mode 100644 src/components/AdvanceAnalyticsV2/WarningBanner.jsx
create mode 100644 src/components/AdvanceAnalyticsV2/tests/WarningBanner.test.jsx
diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
index 1401e6e9a..df6bfe5e0 100644
--- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
+++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
@@ -6,6 +6,7 @@ import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import Cookies from 'universal-cookie';
import Hero from '../Hero';
import Stats from './Stats';
import Enrollments from './tabs/Enrollments';
@@ -14,7 +15,8 @@ import Completions from './tabs/Completions';
import Leaderboard from './tabs/Leaderboard';
import Skills from './tabs/Skills';
import { useEnterpriseAnalyticsAggregatesData } from './data/hooks';
-import { GRANULARITY, CALCULATION } from './data/constants';
+import { GRANULARITY, CALCULATION, ANALYTICS_WARNING_BANNER_COOKIE } from './data/constants';
+import WarningBanner from './WarningBanner';
const PAGE_TITLE = 'AnalyticsV2';
@@ -24,12 +26,14 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
const [calculation, setCalculation] = useState('Total');
const [startDate, setStartDate] = useState();
const [endDate, setEndDate] = useState();
+ const cookies = new Cookies();
const intl = useIntl();
const { isFetching, isError, data } = useEnterpriseAnalyticsAggregatesData({
enterpriseCustomerUUID: enterpriseId,
startDate,
endDate,
});
+ const showWarningBanner = cookies.get(ANALYTICS_WARNING_BANNER_COOKIE);
const currentDate = new Date().toISOString().split('T')[0];
const formatDate = (dateString) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
@@ -39,6 +43,7 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
<>
+ {!showWarningBanner && }
diff --git a/src/components/AdvanceAnalyticsV2/WarningBanner.jsx b/src/components/AdvanceAnalyticsV2/WarningBanner.jsx
new file mode 100644
index 000000000..84bf53320
--- /dev/null
+++ b/src/components/AdvanceAnalyticsV2/WarningBanner.jsx
@@ -0,0 +1,29 @@
+import React, { useState } from 'react';
+import Cookies from 'universal-cookie';
+import {
+ PageBanner,
+} from '@openedx/paragon';
+import { ANALYTICS_WARNING_BANNER_COOKIE } from './data/constants';
+
+const WarningBanner = () => {
+ const [showBanner, setShowBanner] = useState(true);
+ const cookies = new Cookies();
+
+ const onDismiss = () => {
+ setShowBanner(false);
+ cookies.set(ANALYTICS_WARNING_BANNER_COOKIE, true, { sameSite: 'strict' });
+ };
+ return (
+
+ 🚀 Analytics Just Got Better! We've updated charts, improved performance,
+ and now include audit enrollments for a more complete view of your data.
+
+ );
+};
+
+export default WarningBanner;
diff --git a/src/components/AdvanceAnalyticsV2/data/constants.js b/src/components/AdvanceAnalyticsV2/data/constants.js
index bb0a88a39..74cf32ded 100644
--- a/src/components/AdvanceAnalyticsV2/data/constants.js
+++ b/src/components/AdvanceAnalyticsV2/data/constants.js
@@ -114,3 +114,5 @@ export const CALCULATION = {
MOVING_AVERAGE_3_PERIODS: 'moving-average-3-period',
MOVING_AVERAGE_7_PERIODS: 'moving-average-7-period',
};
+
+export const ANALYTICS_WARNING_BANNER_COOKIE = 'hide-warning-banner-on-analytics';
diff --git a/src/components/AdvanceAnalyticsV2/tests/WarningBanner.test.jsx b/src/components/AdvanceAnalyticsV2/tests/WarningBanner.test.jsx
new file mode 100644
index 000000000..38ce81465
--- /dev/null
+++ b/src/components/AdvanceAnalyticsV2/tests/WarningBanner.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import Cookies from 'universal-cookie';
+import WarningBanner from '../WarningBanner';
+import '@testing-library/jest-dom';
+
+// jest.mock('universal-cookie');
+jest.mock('universal-cookie', () => jest.fn().mockImplementation(() => ({
+ set: jest.fn(),
+})));
+
+describe('WarningBanner', () => {
+ let cookies;
+
+ beforeEach(() => {
+ cookies = new Cookies();
+ cookies.set = jest.fn();
+ });
+
+ test('should trigger onDismiss and set cookie in warning banner', () => {
+ render(
);
+
+ // Check that the banner is initially shown
+ expect(screen.getByText(/Analytics Just Got Better!/i)).toBeInTheDocument();
+
+ // Trigger the onDismiss function
+ fireEvent.click(screen.getByRole('button', { name: /Dismiss/i }));
+
+ // Check that the banner is no longer shown
+ expect(screen.queryByText(/Analytics Just Got Better!/i)).not.toBeInTheDocument();
+ });
+});
From bc2aad6cb6c65e47ec0921783a4e2a6a64354f16 Mon Sep 17 00:00:00 2001
From: muhammad-ammar
Date: Wed, 9 Oct 2024 15:27:59 +0500
Subject: [PATCH 16/21] feat: ship analytics 2.0
---
src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx | 2 +-
src/components/EnterpriseApp/EnterpriseAppRoutes.jsx | 8 ++++----
.../EnterpriseApp/EnterpriseAppRoutes.test.jsx | 9 +--------
src/components/EnterpriseApp/data/constants.js | 2 +-
src/components/Sidebar/index.jsx | 6 ------
src/config/index.js | 1 -
6 files changed, 7 insertions(+), 21 deletions(-)
diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
index df6bfe5e0..cde5a8014 100644
--- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
+++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
@@ -18,7 +18,7 @@ import { useEnterpriseAnalyticsAggregatesData } from './data/hooks';
import { GRANULARITY, CALCULATION, ANALYTICS_WARNING_BANNER_COOKIE } from './data/constants';
import WarningBanner from './WarningBanner';
-const PAGE_TITLE = 'AnalyticsV2';
+const PAGE_TITLE = 'Analytics';
const AnalyticsV2Page = ({ enterpriseId }) => {
const [activeTab, setActiveTab] = useState('enrollments');
diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
index 972f7b77f..a44e9f05f 100644
--- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
+++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
@@ -82,17 +82,17 @@ const EnterpriseAppRoutes = ({
/>
)}
- {enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.analytics && (
+ {enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.legacyAnalytics && (
: }
/>
)}
- {enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.analyticsv2 && (
+ {enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.analytics && (
diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.test.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.test.jsx
index d706470f4..22a7d6cc2 100644
--- a/src/components/EnterpriseApp/EnterpriseAppRoutes.test.jsx
+++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.test.jsx
@@ -16,7 +16,7 @@ jest.mock('../PlotlyAnalytics', () => ({
PlotlyAnalyticsPage: () => PlotlyAnalyticsPage Mock Component
,
}));
-let mockEnterpriseAppPage = 'analyticsv2';
+let mockEnterpriseAppPage = 'analytics';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -67,11 +67,4 @@ describe('EnterpriseAppRoutes', () => {
renderWithProviders(defaultProps);
expect(screen.getByText('AdminPage Mock Component')).toBeInTheDocument();
});
-
- it('renders Analytics when ANALYTICS_SUPPORTED is true', () => {
- mockEnterpriseAppPage = 'analytics';
- features.ANALYTICS_SUPPORTED = true;
- renderWithProviders(defaultProps);
- expect(screen.getByText('PlotlyAnalyticsPage Mock Component')).toBeInTheDocument();
- });
});
diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js
index 93bd9412d..71ca6764f 100644
--- a/src/components/EnterpriseApp/data/constants.js
+++ b/src/components/EnterpriseApp/data/constants.js
@@ -1,8 +1,8 @@
/* eslint-disable import/prefer-default-export */
export const ROUTE_NAMES = {
+ legacyAnalytics: 'legacy-analytics',
analytics: 'analytics',
- analyticsv2: 'analyticsv2',
appearance: 'appearance',
bulkEnrollment: 'enrollment',
bulkEnrollmentResults: 'bulk-enrollment-results',
diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx
index bb5f0891f..3913813a5 100644
--- a/src/components/Sidebar/index.jsx
+++ b/src/components/Sidebar/index.jsx
@@ -125,12 +125,6 @@ const Sidebar = ({
icon: ,
hidden: !features.ANALYTICS || !enableAnalyticsScreen,
},
- {
- title: 'AnalyticsV2',
- to: `${baseUrl}/admin/${ROUTE_NAMES.analyticsv2}`,
- icon: ,
- hidden: !features.ANALYTICS_V2,
- },
{
title: 'Code Management',
to: `${baseUrl}/admin/${ROUTE_NAMES.codeManagement}`,
diff --git a/src/config/index.js b/src/config/index.js
index cc3b930b9..b2f4f57f2 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -44,7 +44,6 @@ const features = {
CODE_MANAGEMENT: process.env.FEATURE_CODE_MANAGEMENT || hasFeatureFlagEnabled('CODE_MANAGEMENT'),
REPORTING_CONFIGURATIONS: process.env.FEATURE_REPORTING_CONFIGURATIONS || hasFeatureFlagEnabled('REPORTING_CONFIGURATIONS'),
ANALYTICS: process.env.FEATURE_ANALYTICS || hasFeatureFlagEnabled('ANALYTICS'),
- ANALYTICS_V2: process.env.FEATURE_ANALYTICS_V2 || hasFeatureFlagEnabled('ANALYTICS_V2'),
ANALYTICS_SUPPORTED: process.env.ANALYTICS_SUPPORTED || hasFeatureFlagEnabled('ANALYTICS_SUPPORTED'),
SAML_CONFIGURATION: process.env.FEATURE_SAML_CONFIGURATION || hasFeatureFlagEnabled('SAML_CONFIGURATION'),
SUPPORT: process.env.FEATURE_SUPPORT || hasFeatureFlagEnabled('SUPPORT'),
From 3bb76507524c3f2ae1f4400d8f2daa70503ffcfe Mon Sep 17 00:00:00 2001
From: Marlon Keating <322346+marlonkeating@users.noreply.github.com>
Date: Fri, 22 Nov 2024 11:29:33 -0800
Subject: [PATCH 17/21] fix: refresh group list after adding group (#1349)
fix: Invalidate groups query after new members are invited
---
src/components/PeopleManagement/CreateGroupModal.jsx | 6 ++++++
.../PeopleManagement/tests/PeopleManagementPage.test.jsx | 5 +++++
2 files changed, 11 insertions(+)
diff --git a/src/components/PeopleManagement/CreateGroupModal.jsx b/src/components/PeopleManagement/CreateGroupModal.jsx
index 6630c65cf..16b780286 100644
--- a/src/components/PeopleManagement/CreateGroupModal.jsx
+++ b/src/components/PeopleManagement/CreateGroupModal.jsx
@@ -2,6 +2,7 @@ import React, { useCallback, useState, useEffect } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+import { useQueryClient } from '@tanstack/react-query';
import { useIntl } from '@edx/frontend-platform/i18n';
import { snakeCaseObject } from '@edx/frontend-platform/utils';
import {
@@ -10,6 +11,7 @@ import {
import LmsApiService from '../../data/services/LmsApiService';
import SystemErrorAlertModal from '../learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal';
import CreateGroupModalContent from './CreateGroupModalContent';
+import { learnerCreditManagementQueryKeys } from '../learner-credit-management/data';
const CreateGroupModal = ({
isModalOpen,
@@ -27,6 +29,7 @@ const CreateGroupModal = ({
closeModal();
setCreateButtonState('default');
};
+ const queryClient = useQueryClient();
const handleCreateGroup = async () => {
setCreateButtonState('pending');
@@ -49,6 +52,9 @@ const CreateGroupModal = ({
learnerEmails,
});
await LmsApiService.inviteEnterpriseLearnersToGroup(groupCreationResponse.data.uuid, requestBody);
+ queryClient.invalidateQueries({
+ queryKey: learnerCreditManagementQueryKeys.group(enterpriseUUID),
+ });
setCreateButtonState('complete');
handleCloseCreateGroupModal();
} catch (err) {
diff --git a/src/components/PeopleManagement/tests/PeopleManagementPage.test.jsx b/src/components/PeopleManagement/tests/PeopleManagementPage.test.jsx
index 03f15c5f2..fb69b2f23 100644
--- a/src/components/PeopleManagement/tests/PeopleManagementPage.test.jsx
+++ b/src/components/PeopleManagement/tests/PeopleManagementPage.test.jsx
@@ -33,6 +33,11 @@ const subsEnterpriseSubsidiesContextValue = {
isLoading: false,
};
+jest.mock('@tanstack/react-query', () => ({
+ ...jest.requireActual('@tanstack/react-query'),
+ useQueryClient: jest.fn(),
+}));
+
jest.mock('../../learner-credit-management/data', () => ({
...jest.requireActual('../../learner-credit-management/data'),
useAllEnterpriseGroups: jest.fn(),
From c9bd93f8d8138417bd4dfc4651eff717f4092bbb Mon Sep 17 00:00:00 2001
From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com>
Date: Tue, 26 Nov 2024 08:16:33 -0800
Subject: [PATCH 18/21] feat: verify learner email belongs to org (#1356)
---
.../PeopleManagement/CreateGroupModal.jsx | 1 +
.../CreateGroupModalContent.jsx | 8 +++-
.../LearnerNotInOrgErrorState.jsx | 36 ++++++++++++++
.../tests/CreateGroupModal.test.jsx | 31 ++++++++++++
.../cards/data/utils.js | 16 ++++++-
.../data/hooks/index.js | 1 +
.../tests/useEnterpriseLearners.test.jsx | 48 +++++++++++++++++++
.../data/hooks/useEnterpriseLearners.js | 34 +++++++++++++
.../invite-modal/InviteModalContent.jsx | 7 ++-
.../invite-modal/InviteModalInputFeedback.jsx | 8 ++++
.../invite-modal/InviteModalSummary.jsx | 10 ++++
.../invite-modal/InviteSummaryCount.jsx | 1 +
12 files changed, 198 insertions(+), 3 deletions(-)
create mode 100644 src/components/PeopleManagement/LearnerNotInOrgErrorState.jsx
create mode 100644 src/components/learner-credit-management/data/hooks/tests/useEnterpriseLearners.test.jsx
create mode 100644 src/components/learner-credit-management/data/hooks/useEnterpriseLearners.js
diff --git a/src/components/PeopleManagement/CreateGroupModal.jsx b/src/components/PeopleManagement/CreateGroupModal.jsx
index 16b780286..ecf7e9aae 100644
--- a/src/components/PeopleManagement/CreateGroupModal.jsx
+++ b/src/components/PeopleManagement/CreateGroupModal.jsx
@@ -113,6 +113,7 @@ const CreateGroupModal = ({
onSetGroupName={setGroupName}
onEmailAddressesChange={handleEmailAddressesChange}
isGroupInvite
+ enterpriseUUID={enterpriseUUID}
/>
{
const [learnerEmails, setLearnerEmails] = useState([]);
const [emailAddressesInputValue, setEmailAddressesInputValue] = useState('');
@@ -26,9 +28,11 @@ const CreateGroupModalContent = ({
isValidInput: null,
lowerCasedEmails: [],
duplicateEmails: [],
+ emailsNotInOrg: [],
});
const [groupNameLength, setGroupNameLength] = useState(0);
const [groupName, setGroupName] = useState('');
+ const { allEnterpriseLearners } = useEnterpriseLearners({ enterpriseUUID });
const handleGroupNameChange = useCallback((e) => {
if (!e.target.value) {
@@ -86,6 +90,7 @@ const CreateGroupModalContent = ({
useEffect(() => {
const inviteMetadata = isInviteEmailAddressesInputValueValid({
learnerEmails,
+ allEnterpriseLearners,
});
setMemberInviteMetadata(inviteMetadata);
if (inviteMetadata.canInvite) {
@@ -93,7 +98,7 @@ const CreateGroupModalContent = ({
} else {
onEmailAddressesChange([]);
}
- }, [onEmailAddressesChange, learnerEmails]);
+ }, [onEmailAddressesChange, learnerEmails, allEnterpriseLearners]);
return (
@@ -155,6 +160,7 @@ CreateGroupModalContent.propTypes = {
onEmailAddressesChange: PropTypes.func.isRequired,
onSetGroupName: PropTypes.func,
isGroupInvite: PropTypes.bool,
+ enterpriseUUID: PropTypes.string.isRequired,
};
export default CreateGroupModalContent;
diff --git a/src/components/PeopleManagement/LearnerNotInOrgErrorState.jsx b/src/components/PeopleManagement/LearnerNotInOrgErrorState.jsx
new file mode 100644
index 000000000..c4421c31c
--- /dev/null
+++ b/src/components/PeopleManagement/LearnerNotInOrgErrorState.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import {
+ Card, Hyperlink, Stack, Icon,
+} from '@openedx/paragon';
+import classNames from 'classnames';
+import { Error } from '@openedx/paragon/icons';
+
+const LearnerNotInOrgErrorState = () => (
+
+
+
+
+
+
+
Some people can't be added.
+
Check that all people in the file are registered with your organization.
+
+ Learn more
+
+
+
+
+
+
+
+);
+
+export default LearnerNotInOrgErrorState;
diff --git a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
index ba55f11ad..ca0d39fe0 100644
--- a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
+++ b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
@@ -17,6 +17,7 @@ import {
useEnterpriseLearnersTableData,
useGetAllEnterpriseLearnerEmails,
} from '../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData';
+import { useEnterpriseLearners } from '../../learner-credit-management/data';
jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'),
@@ -28,6 +29,10 @@ jest.mock('../../learner-credit-management/data/hooks/useEnterpriseLearnersTable
useEnterpriseLearnersTableData: jest.fn(),
useGetAllEnterpriseLearnerEmails: jest.fn(),
}));
+jest.mock('../../learner-credit-management/data', () => ({
+ ...jest.requireActual('../../learner-credit-management/data'),
+ useEnterpriseLearners: jest.fn(),
+}));
const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
@@ -118,6 +123,9 @@ describe('', () => {
fetchLearnerEmails: jest.fn(),
addButtonState: 'complete',
});
+ useEnterpriseLearners.mockReturnValue({
+ allEnterpriseLearners: ['testuser-3@2u.com', 'testuser-2@2u.com', 'testuser-1@2u.com', 'tomhaverford@pawnee.org'],
+ });
});
it('Modal renders as expected', async () => {
render();
@@ -204,6 +212,29 @@ describe('', () => {
expect(mockInvite).toHaveBeenCalledTimes(1);
});
});
+ it('displays error for email not belonging in an org', async () => {
+ const mockGroupData = { uuid: 'test-uuid' };
+ LmsApiService.createEnterpriseGroup.mockResolvedValue({ status: 201, data: mockGroupData });
+
+ const mockInviteData = { records_processed: 1, new_learners: 1, existing_learners: 0 };
+ LmsApiService.inviteEnterpriseLearnersToGroup.mockResolvedValue(mockInviteData);
+ useEnterpriseLearners.mockReturnValue({
+ allEnterpriseLearners: ['testuser-3@2u.com'],
+ });
+ render();
+ const groupNameInput = screen.getByTestId('group-name');
+ userEvent.type(groupNameInput, 'test group name');
+ const fakeFile = new File(['tomhaverford@pawnee.org'], 'emails.csv', { type: 'text/csv' });
+ const dropzone = screen.getByText('Drag and drop your file here or click to upload.');
+ Object.defineProperty(dropzone, 'files', {
+ value: [fakeFile],
+ });
+ fireEvent.drop(dropzone);
+ await waitFor(() => {
+ expect(screen.getByText(/Some people can't be added/i)).toBeInTheDocument();
+ expect(/tomhaverford@pawnee.org email address is not available to be added to a group./i);
+ }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
+ });
it('displays system error modal', async () => {
const mockCreateGroup = jest.spyOn(LmsApiService, 'createEnterpriseGroup');
const mockInvite = jest.spyOn(LmsApiService, 'inviteEnterpriseLearnersToGroup');
diff --git a/src/components/learner-credit-management/cards/data/utils.js b/src/components/learner-credit-management/cards/data/utils.js
index 036d3c8bd..0015bc80e 100644
--- a/src/components/learner-credit-management/cards/data/utils.js
+++ b/src/components/learner-credit-management/cards/data/utils.js
@@ -119,12 +119,13 @@ export const isAssignEmailAddressesInputValueValid = ({
* input, including a validation error when appropriate, and whether the member invitation
* should proceed.
*/
-export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
+export const isInviteEmailAddressesInputValueValid = ({ learnerEmails, allEnterpriseLearners = null }) => {
let validationError;
const learnerEmailsCount = learnerEmails.length;
const lowerCasedEmails = [];
const invalidEmails = [];
const duplicateEmails = [];
+ const emailsNotInOrg = [];
learnerEmails.forEach((email) => {
const lowerCasedEmail = email.toLowerCase();
@@ -135,6 +136,9 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
} else if (lowerCasedEmails.includes(lowerCasedEmail)) {
// Check for duplicates (case-insensitive)
duplicateEmails.push(email);
+ // Check if email belongs in the org
+ } else if (allEnterpriseLearners && !allEnterpriseLearners.includes(email)) {
+ emailsNotInOrg.push(email);
} else {
// Add to list of lower-cased emails already handled
lowerCasedEmails.push(lowerCasedEmail);
@@ -174,6 +178,15 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
ensureValidationErrorObjectExists();
validationError.reason = 'duplicate_email';
validationError.message = message;
+ } else if (emailsNotInOrg.length > 0) {
+ let message = `${emailsNotInOrg[0]} is not available to be added to a group.`;
+ if (emailsNotInOrg.length > 1) {
+ message = `${emailsNotInOrg[0]} and ${makePlural(emailsNotInOrg.length - 1, 'other email address')}
+ are not available to be added to a group.`;
+ }
+ ensureValidationErrorObjectExists();
+ validationError.reason = 'email_not_in_org';
+ validationError.message = message;
}
return {
canInvite,
@@ -182,5 +195,6 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
invalidEmails,
isValidInput,
validationError,
+ emailsNotInOrg,
};
};
diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js
index 6f1842953..21e5e65f8 100644
--- a/src/components/learner-credit-management/data/hooks/index.js
+++ b/src/components/learner-credit-management/data/hooks/index.js
@@ -23,3 +23,4 @@ export { default as useContentMetadata } from './useContentMetadata';
export { default as useEnterpriseRemovedGroupMembers } from './useEnterpriseRemovedGroupMembers';
export { default as useEnterpriseFlexGroups } from './useEnterpriseFlexGroups';
export { default as useGroupDropdownToggle } from './useGroupDropdownToggle';
+export { default as useEnterpriseLearners } from './useEnterpriseLearners';
diff --git a/src/components/learner-credit-management/data/hooks/tests/useEnterpriseLearners.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseLearners.test.jsx
new file mode 100644
index 000000000..ab9b1e6c3
--- /dev/null
+++ b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseLearners.test.jsx
@@ -0,0 +1,48 @@
+import { QueryClientProvider } from '@tanstack/react-query';
+import { renderHook } from '@testing-library/react-hooks';
+
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import LmsApiService from '../../../../../data/services/LmsApiService';
+import { queryClient } from '../../../../test/testUtils';
+import useEnterpriseLearners from '../useEnterpriseLearners';
+
+jest.mock('../../../../../data/services/LmsApiService', () => ({
+ fetchEnterpriseLearnerData: jest.fn(),
+}));
+
+jest.mock('@edx/frontend-platform/utils', () => ({
+ camelCaseObject: jest.fn(),
+}));
+
+const wrapper = ({ children }) => (
+ {children}
+);
+
+describe('useEnterpriseLearners', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fetch and return enterprise learners', async () => {
+ const mockData = [
+ {
+ user: {
+ email: 'test@2u.com',
+ },
+ },
+ ];
+ LmsApiService.fetchEnterpriseLearnerData.mockResolvedValue(mockData);
+ camelCaseObject.mockResolvedValue(mockData);
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useEnterpriseLearners({ enterpriseUUID: 'test-id' }),
+ { wrapper },
+ );
+ await waitForNextUpdate();
+ expect(LmsApiService.fetchEnterpriseLearnerData).toHaveBeenCalledWith({
+ enterprise_customer: 'test-id',
+ });
+ expect(camelCaseObject).toHaveBeenCalledWith(mockData);
+ expect(result.current.allEnterpriseLearners).toEqual(['test@2u.com']);
+ });
+});
diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseLearners.js b/src/components/learner-credit-management/data/hooks/useEnterpriseLearners.js
new file mode 100644
index 000000000..ab4bcc595
--- /dev/null
+++ b/src/components/learner-credit-management/data/hooks/useEnterpriseLearners.js
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react';
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import { logError } from '@edx/frontend-platform/logging';
+
+import LmsApiService from '../../../../data/services/LmsApiService';
+
+const useEnterpriseLearners = ({
+ enterpriseUUID,
+}) => {
+ const [allEnterpriseLearners, setAllEnterpriseLearners] = useState([]);
+
+ useEffect(() => {
+ const fetchLearnerEmails = async () => {
+ try {
+ const options = {
+ enterprise_customer: enterpriseUUID,
+ };
+ const data = await LmsApiService.fetchEnterpriseLearnerData(options);
+ const results = await camelCaseObject(data);
+ const learnerEmails = results.map(result => result?.user?.email);
+ setAllEnterpriseLearners(learnerEmails);
+ } catch (error) {
+ logError(error);
+ }
+ };
+ fetchLearnerEmails();
+ }, [enterpriseUUID]);
+
+ return {
+ allEnterpriseLearners,
+ };
+};
+
+export default useEnterpriseLearners;
diff --git a/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx b/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
index 3495e9a89..426f468ae 100644
--- a/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteModalContent.jsx
@@ -31,7 +31,12 @@ const InviteModalContent = ({
const [learnerEmails, setLearnerEmails] = useState([]);
const [inputType, setInputType] = useState('email');
const [emailAddressesInputValue, setEmailAddressesInputValue] = useState('');
- const [memberInviteMetadata, setMemberInviteMetadata] = useState({});
+ const [memberInviteMetadata, setMemberInviteMetadata] = useState({
+ isValidInput: null,
+ lowerCasedEmails: [],
+ duplicateEmails: [],
+ emailsNotInOrg: [],
+ });
const [groupMemberEmails, setGroupMemberEmails] = useState([]);
const [checkedGroups, setCheckedGroups] = useState({});
const [dropdownToggleLabel, setDropdownToggleLabel] = useState(GROUP_DROPDOWN_TEXT);
diff --git a/src/components/learner-credit-management/invite-modal/InviteModalInputFeedback.jsx b/src/components/learner-credit-management/invite-modal/InviteModalInputFeedback.jsx
index fec235f45..ab2916a75 100644
--- a/src/components/learner-credit-management/invite-modal/InviteModalInputFeedback.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteModalInputFeedback.jsx
@@ -11,6 +11,13 @@ const InviteModalInputFeedback = ({ memberInviteMetadata, isCsvUpload }) => {
);
}
+ if (memberInviteMetadata.emailsNotInOrg.length > 0) {
+ return (
+
+ {memberInviteMetadata.validationError.message}
+
+ );
+ }
return (
{memberInviteMetadata.validationError.message}
@@ -46,6 +53,7 @@ InviteModalInputFeedback.propTypes = {
lowerCasedEmails: PropTypes.arrayOf(
PropTypes.shape({}),
),
+ emailsNotInOrg: PropTypes.arrayOf(PropTypes.string),
}),
isCsvUpload: PropTypes.bool,
};
diff --git a/src/components/learner-credit-management/invite-modal/InviteModalSummary.jsx b/src/components/learner-credit-management/invite-modal/InviteModalSummary.jsx
index 22536bf17..e7f17fc80 100644
--- a/src/components/learner-credit-management/invite-modal/InviteModalSummary.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteModalSummary.jsx
@@ -8,6 +8,7 @@ import InviteModalSummaryEmptyState from './InviteModalSummaryEmptyState';
import InviteModalSummaryLearnerList from './InviteModalSummaryLearnerList';
import InviteModalSummaryErrorState from './InviteModalSummaryErrorState';
import InviteModalSummaryDuplicate from './InviteModalSummaryDuplicate';
+import LearnerNotInOrgErrorState from '../../PeopleManagement/LearnerNotInOrgErrorState';
const InviteModalSummary = ({
memberInviteMetadata,
@@ -17,7 +18,9 @@ const InviteModalSummary = ({
isValidInput,
lowerCasedEmails,
duplicateEmails,
+ emailsNotInOrg,
} = memberInviteMetadata;
+ const hasEmailsNotInOrg = emailsNotInOrg.length > 0;
const renderCard = (contents, showErrorHighlight) => (
,
+ );
+ }
+
if (isEmpty(cardSections)) {
cardSections = cardSections.concat(
renderCard(),
@@ -71,6 +80,7 @@ InviteModalSummary.propTypes = {
isValidInput: PropTypes.bool,
lowerCasedEmails: PropTypes.arrayOf(PropTypes.string),
duplicateEmails: PropTypes.arrayOf(PropTypes.string),
+ emailsNotInOrg: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
isGroupInvite: PropTypes.bool,
};
diff --git a/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx b/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
index a87836747..171a48c4d 100644
--- a/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
+++ b/src/components/learner-credit-management/invite-modal/InviteSummaryCount.jsx
@@ -18,6 +18,7 @@ InviteSummaryCount.propTypes = {
isValidInput: PropTypes.bool,
lowerCasedEmails: PropTypes.arrayOf(PropTypes.string),
duplicateEmails: PropTypes.arrayOf(PropTypes.string),
+ emailsNotInOrg: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
From 0859fe5f60d666931d1c0fa1a2255617353c2318 Mon Sep 17 00:00:00 2001
From: Saleem Latif
Date: Wed, 27 Nov 2024 16:15:38 +0500
Subject: [PATCH 19/21] refactor: Removed unncessary code.
---
.../EnterpriseApp/EnterpriseAppRoutes.jsx | 9 ---
.../EnterpriseAppRoutes.test.jsx | 3 -
.../EnterpriseApp/data/constants.js | 1 -
.../PlotlyAnalytics/PlotlyAnalyticsCharts.jsx | 63 -------------------
.../PlotlyAnalytics/PlotlyAnalyticsPage.jsx | 62 ------------------
.../PlotlyAnalytics/data/service.jsx | 18 ------
src/components/PlotlyAnalytics/index.jsx | 2 -
7 files changed, 158 deletions(-)
delete mode 100644 src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx
delete mode 100644 src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx
delete mode 100644 src/components/PlotlyAnalytics/data/service.jsx
delete mode 100644 src/components/PlotlyAnalytics/index.jsx
diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
index a44e9f05f..731b34ff9 100644
--- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
+++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
@@ -11,7 +11,6 @@ import NotFoundPage from '../NotFoundPage';
import LoadingMessage from '../LoadingMessage';
import SettingsPage from '../settings';
import { SubscriptionManagementPage } from '../subscriptions';
-import { PlotlyAnalyticsPage } from '../PlotlyAnalytics';
import AnalyticsV2Page from '../AdvanceAnalyticsV2/AnalyticsV2Page';
import FeatureNotSupportedPage from '../FeatureNotSupportedPage';
import { ROUTE_NAMES } from './data/constants';
@@ -82,14 +81,6 @@ const EnterpriseAppRoutes = ({
/>
)}
- {enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.legacyAnalytics && (
- : }
- />
- )}
-
{enableAnalyticsPage && enterpriseAppPage === ROUTE_NAMES.analytics && (
function AnalyticsV2Pag
jest.mock('../../containers/AdminPage', () => function AdminPageMock() {
return AdminPage Mock Component
;
});
-jest.mock('../PlotlyAnalytics', () => ({
- PlotlyAnalyticsPage: () => PlotlyAnalyticsPage Mock Component
,
-}));
let mockEnterpriseAppPage = 'analytics';
diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js
index 71ca6764f..1a809ec2d 100644
--- a/src/components/EnterpriseApp/data/constants.js
+++ b/src/components/EnterpriseApp/data/constants.js
@@ -1,7 +1,6 @@
/* eslint-disable import/prefer-default-export */
export const ROUTE_NAMES = {
- legacyAnalytics: 'legacy-analytics',
analytics: 'analytics',
appearance: 'appearance',
bulkEnrollment: 'enrollment',
diff --git a/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx b/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx
deleted file mode 100644
index d5562f51a..000000000
--- a/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { DashApp } from 'dash-embedded-component';
-import { logError } from '@edx/frontend-platform/logging';
-import PropTypes from 'prop-types';
-import LoadingMessage from '../LoadingMessage';
-import ErrorPage from '../ErrorPage';
-import PlotlyAnalyticsApiService from './data/service';
-import { configuration } from '../../config';
-
-const PlotlyAnalyticsCharts = ({ enterpriseId, enableDemoData }) => {
- const [token, setToken] = useState('');
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const enterpriseUUID = enableDemoData ? configuration.DEMO_ENTEPRISE_UUID : enterpriseId;
-
- const refreshPlotlyToken = async () => {
- const response = await PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseUUID });
- return response.data.token;
- };
-
- useEffect(() => {
- setIsLoading(true);
- PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseUUID })
- .then((response) => {
- setToken(response.data.token);
- setIsLoading(false);
- })
- .catch((err) => {
- logError(err);
- setIsLoading(false);
- setError(err);
- });
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
-
- if (isLoading) {
- return ;
- }
- if (error) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-PlotlyAnalyticsCharts.propTypes = {
- enterpriseId: PropTypes.string.isRequired,
- enableDemoData: PropTypes.bool.isRequired,
-};
-
-export default PlotlyAnalyticsCharts;
diff --git a/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx b/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx
deleted file mode 100644
index b512f5375..000000000
--- a/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useState } from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import { Helmet } from 'react-helmet';
-
-import { Alert } from '@openedx/paragon';
-import { CheckCircle, Error } from '@openedx/paragon/icons';
-import Hero from '../Hero';
-import PlotlyAnalyticsCharts from './PlotlyAnalyticsCharts';
-
-const PAGE_TITLE = 'Analytics';
-
-const PlotlyAnalyticsPage = ({ enterpriseId, enableDemoData }) => {
- const [status, setStatus] = useState({
- visible: false, alertType: '', message: '',
- });
-
- const setSuccessStatus = ({ visible, message = '' }) => {
- setStatus({
- visible,
- alertType: 'success',
- message,
- });
- };
-
- const renderStatusMessage = () => (
- status && status.visible && (
- setSuccessStatus({ visible: false })}
- dismissible
- >
- {status.title}
- {status.message}
-
- )
- );
-
- return (
- <>
-
-
-
- {renderStatusMessage()}
-
-
- >
- );
-};
-
-PlotlyAnalyticsPage.propTypes = {
- enterpriseId: PropTypes.string.isRequired,
- enableDemoData: PropTypes.bool.isRequired,
-};
-
-const mapStateToProps = state => ({
- enterpriseId: state.portalConfiguration.enterpriseId,
- enableDemoData: state.portalConfiguration.enableDemoData,
-});
-
-export default connect(mapStateToProps)(PlotlyAnalyticsPage);
diff --git a/src/components/PlotlyAnalytics/data/service.jsx b/src/components/PlotlyAnalytics/data/service.jsx
deleted file mode 100644
index 5156f5b83..000000000
--- a/src/components/PlotlyAnalytics/data/service.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-import { configuration } from '../../../config';
-
-class PlotlyAnalyticsApiService {
- static lmsBaseUrl = configuration.LMS_BASE_URL;
-
- static plotlyTokenUrl = `${PlotlyAnalyticsApiService.lmsBaseUrl}/enterprise/api/v1/plotly_token`;
-
- static fetchPlotlyToken(options) {
- // eslint-disable-next-line no-unused-vars
- const queryParams = {
- ...options,
- };
- return getAuthenticatedHttpClient().get(`${PlotlyAnalyticsApiService.plotlyTokenUrl}/${queryParams.enterpriseUUID}`);
- }
-}
-
-export default PlotlyAnalyticsApiService;
diff --git a/src/components/PlotlyAnalytics/index.jsx b/src/components/PlotlyAnalytics/index.jsx
deleted file mode 100644
index 197bfcfbd..000000000
--- a/src/components/PlotlyAnalytics/index.jsx
+++ /dev/null
@@ -1,2 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export { default as PlotlyAnalyticsPage } from './PlotlyAnalyticsPage';
From 927f8c0855aa9bac36e2154c7f03b262d68ca593 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Dec 2024 17:01:19 -0500
Subject: [PATCH 20/21] chore(deps): bump axios from 1.7.3 to 1.7.5 (#1283)
Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.5)
---
updated-dependencies:
- dependency-name: axios
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hamzah Ullah
---
package-lock.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d3cb9369b..816d7c99b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7205,8 +7205,9 @@
}
},
"node_modules/axios": {
- "version": "1.7.3",
- "license": "MIT",
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
+ "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
From 7d6cf54cf365864111d3fd2e8635ec9a6e3433e0 Mon Sep 17 00:00:00 2001
From: jajjibhai008
Date: Wed, 20 Nov 2024 15:01:37 +0500
Subject: [PATCH 21/21] feat: update pagination for reporting configurations
---
src/data/services/LmsApiService.js | 2 +-
src/data/services/tests/LmsApiService.test.js | 25 +++++++++++++++++++
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 7fd5fc65e..40a388e1a 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -120,7 +120,7 @@ class LmsApiService {
}
static fetchReportingConfigs(uuid) {
- return LmsApiService.apiClient().get(`${LmsApiService.reportingConfigUrl}?enterprise_customer=${uuid}`);
+ return LmsApiService.apiClient().get(`${LmsApiService.reportingConfigUrl}?enterprise_customer=${uuid}&page_size=100`);
}
static fetchReportingConfigTypes(uuid) {
diff --git a/src/data/services/tests/LmsApiService.test.js b/src/data/services/tests/LmsApiService.test.js
index b74d78846..67d86e459 100644
--- a/src/data/services/tests/LmsApiService.test.js
+++ b/src/data/services/tests/LmsApiService.test.js
@@ -114,4 +114,29 @@ describe('LmsApiService', () => {
},
});
});
+ test('fetchReportingConfigs returns reporting configs', async () => {
+ axios.get.mockResolvedValue({
+ status: 200,
+ data: {
+ results: [{
+ active: true,
+ data_type: 'test-data-type',
+ uuid: 'test-uuid',
+ enterprise_customer: 'test-enterprise-customer',
+ }],
+ },
+ });
+ const response = await LmsApiService.fetchReportingConfigs();
+ expect(response).toEqual({
+ status: 200,
+ data: {
+ results: [{
+ active: true,
+ data_type: 'test-data-type',
+ uuid: 'test-uuid',
+ enterprise_customer: 'test-enterprise-customer',
+ }],
+ },
+ });
+ });
});