Skip to content

Commit 9f4b391

Browse files
Merge pull request #1720 from pbitla-sonata/pbitla/ENT-11145
Add unenrollment filter and update filter order in Learner Progress Report UI
2 parents f1e27b2 + cf12ed3 commit 9f4b391

File tree

5 files changed

+222
-69
lines changed

5 files changed

+222
-69
lines changed

src/components/AdminV2/AdminSearchForm.jsx

Lines changed: 117 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import SearchBar from '../SearchBar';
1919
const AdminSearchForm = ({
2020
searchEnrollmentsList,
2121
searchParams: {
22-
searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery, searchGroupQuery,
22+
searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery, searchGroupQuery, searchEnrollmentQuery,
2323
},
2424
tableData = [],
2525
budgets,
@@ -37,7 +37,8 @@ const AdminSearchForm = ({
3737
return;
3838
}
3939
searchEnrollmentsList();
40-
}, [searchEnrollmentsList, searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery, searchGroupQuery]);
40+
}, [searchEnrollmentsList, searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery, searchGroupQuery,
41+
searchEnrollmentQuery]);
4142

4243
const onCourseSelect = (event) => {
4344
const updateParams = {
@@ -71,6 +72,19 @@ const AdminSearchForm = ({
7172
);
7273
};
7374

75+
const onEnrollmentSelect = (event) => {
76+
const updateParams = {
77+
search_enrollment: event.target.value,
78+
page: 1,
79+
};
80+
updateUrl(navigate, location.pathname, updateParams);
81+
sendEnterpriseTrackEvent(
82+
enterpriseId,
83+
EVENT_NAMES.LEARNER_PROGRESS_REPORT.FILTER_BY_ENROLLMENT_DROPDOWN,
84+
{ enrollment: event.target.value },
85+
);
86+
};
87+
7488
const courseTitles = Array.from(new Set(tableData.map(en => en.course_title).sort()));
7589
const courseDates = Array.from(new Set(tableData.map(en => en.course_start_date).sort().reverse()));
7690
const columnWidth = (budgets?.length || groups?.length) ? 'col-md-3' : 'col-md-6';
@@ -79,6 +93,32 @@ const AdminSearchForm = ({
7993
<div className="row" id={TRACK_LEARNER_PROGRESS_TARGETS.FILTER}>
8094
<div className="col-12 pr-md-0 mb-0">
8195
<div className="row w-100 m-0">
96+
<div className={classNames('col-12 my-2 my-md-0 px-0 px-md-2 px-lg-3', columnWidth)}>
97+
<Form.Label id="search-email-label" className="mb-2">
98+
<FormattedMessage
99+
id="admin.portal.lpr.filter.by.email.input.label"
100+
defaultMessage="Filter by email"
101+
description="Label for the email filter dropdown in the admin portal LPR page"
102+
/>
103+
</Form.Label>
104+
<SearchBar
105+
data-testid="admin-search-bar"
106+
placeholder={intl.formatMessage({
107+
id: 'admin.portal.lpr.filter.by.email.input.placeholder',
108+
defaultMessage: 'Search by email...',
109+
description: 'Placeholder text for the email filter input in the admin portal LPR page.',
110+
})}
111+
onSearch={query => updateUrl(navigate, location.pathname, {
112+
search: query,
113+
page: 1,
114+
})}
115+
onClear={() => updateUrl(navigate, location.pathname, { search: undefined })}
116+
value={searchQuery}
117+
aria-labelledby="search-email-label"
118+
className="py-0"
119+
inputProps={{ 'data-hj-suppress': true }}
120+
/>
121+
</div>
82122
{groups?.length ? (
83123
<div className="col-12 col-md-3 my-2 my-md-0 px-0 px-md-2 px-lg-3">
84124
<Form.Group>
@@ -115,7 +155,79 @@ const AdminSearchForm = ({
115155
</Form.Group>
116156
</div>
117157
) : null}
118-
158+
{budgets?.length ? (
159+
<div className="col-12 col-md-3 my-2 my-md-0 px-0 px-md-2 px-lg-3">
160+
<Form.Group>
161+
<Form.Label className="search-label mb-2">
162+
<FormattedMessage
163+
id="admin.portal.lpr.filter.by.budget.dropdown.label"
164+
defaultMessage="Filter by budget"
165+
description="Label for the budget filter dropdown in the admin portal LPR page."
166+
/>
167+
</Form.Label>
168+
<Form.Control
169+
data-testid="admin-search-form-control"
170+
className="w-100 budgets-dropdown"
171+
as="select"
172+
value={searchBudgetQuery}
173+
onChange={e => onBudgetSelect(e)}
174+
>
175+
<option value="">
176+
{intl.formatMessage({
177+
id: 'admin.portal.lpr.filter.by.budget.dropdown.option.all.budgets',
178+
defaultMessage: 'All budgets',
179+
description: 'Label for the all budgets option in the budget filter dropdown in the admin portal LPR page.',
180+
})}
181+
</option>
182+
{budgets.map(budget => (
183+
<option
184+
value={budget.subsidy_access_policy_uuid}
185+
key={budget.subsidy_access_policy_uuid}
186+
>
187+
{budget.subsidy_access_policy_display_name}
188+
</option>
189+
))}
190+
</Form.Control>
191+
</Form.Group>
192+
</div>
193+
) : null }
194+
{/* Filter by Enrollment */}
195+
<div className="col-12 col-md-3 my-2 my-md-0 px-0 px-md-2 px-lg-3">
196+
<Form.Group>
197+
<Form.Label className="search-label mb-2">
198+
<FormattedMessage
199+
id="admin.portal.lpr.filter.by.enrollment.dropdown.label"
200+
defaultMessage="Filter by enrollment"
201+
/>
202+
</Form.Label>
203+
<Form.Control
204+
data-testid="admin-search-form-control"
205+
className="w-100 enrollments-dropdown"
206+
as="select"
207+
value={searchEnrollmentQuery}
208+
onChange={e => onEnrollmentSelect(e)}
209+
>
210+
<option value="">
211+
{intl.formatMessage({
212+
id: 'admin.portal.lpr.filter.by.enrollment.dropdown.option.all',
213+
defaultMessage: 'All',
214+
})}
215+
</option>
216+
<option value="enrolled">
217+
{intl.formatMessage({
218+
id: 'admin.portal.lpr.filter.by.enrollment.dropdown.option.enrolled',
219+
defaultMessage: 'Enrolled',
220+
})}
221+
</option>
222+
<option value="unenrolled">
223+
{intl.formatMessage({
224+
id: 'admin.portal.lpr.filter.by.enrollment.dropdown.option.unenrolled',
225+
defaultMessage: 'Unenrolled',
226+
})}
227+
</option>
228+
</Form.Control>
229+
</Form.Group>
230+
</div>
119231
<div className="col-12 col-md-3 px-0 pl-0 pr-md-2 pr-lg-3">
120232
<Form.Group>
121233
<Form.Label className="search-label mb-2">
@@ -211,68 +323,7 @@ const AdminSearchForm = ({
211323
</Form.Control>
212324
</Form.Group>
213325
</div>
214-
{budgets?.length ? (
215-
<div className="col-12 col-md-3 my-2 my-md-0 px-0 px-md-2 px-lg-3">
216-
<Form.Group>
217-
<Form.Label className="search-label mb-2">
218-
<FormattedMessage
219-
id="admin.portal.lpr.filter.by.budget.dropdown.label"
220-
defaultMessage="Filter by budget"
221-
description="Label for the budget filter dropdown in the admin portal LPR page."
222-
/>
223-
</Form.Label>
224-
<Form.Control
225-
data-testid="admin-search-form-control"
226-
className="w-100 budgets-dropdown"
227-
as="select"
228-
value={searchBudgetQuery}
229-
onChange={e => onBudgetSelect(e)}
230-
>
231-
<option value="">
232-
{intl.formatMessage({
233-
id: 'admin.portal.lpr.filter.by.budget.dropdown.option.all.budgets',
234-
defaultMessage: 'All budgets',
235-
description: 'Label for the all budgets option in the budget filter dropdown in the admin portal LPR page.',
236-
})}
237-
</option>
238-
{budgets.map(budget => (
239-
<option
240-
value={budget.subsidy_access_policy_uuid}
241-
key={budget.subsidy_access_policy_uuid}
242-
>
243-
{budget.subsidy_access_policy_display_name}
244-
</option>
245-
))}
246-
</Form.Control>
247-
</Form.Group>
248-
</div>
249-
) : null }
250-
<div className={classNames('col-12 my-2 my-md-0 px-0 px-md-2 px-lg-3', columnWidth)}>
251-
<Form.Label id="search-email-label" className="mb-2">
252-
<FormattedMessage
253-
id="admin.portal.lpr.filter.by.email.input.label"
254-
defaultMessage="Filter by email"
255-
description="Label for the email filter dropdown in the admin portal LPR page"
256-
/>
257-
</Form.Label>
258-
<SearchBar
259-
data-testid="admin-search-bar"
260-
placeholder={intl.formatMessage({
261-
id: 'admin.portal.lpr.filter.by.email.input.placeholder',
262-
defaultMessage: 'Search by email...',
263-
description: 'Placeholder text for the email filter input in the admin portal LPR page.',
264-
})}
265-
onSearch={query => updateUrl(navigate, location.pathname, {
266-
search: query,
267-
page: 1,
268-
})}
269-
onClear={() => updateUrl(navigate, location.pathname, { search: undefined })}
270-
value={searchQuery}
271-
aria-labelledby="search-email-label"
272-
className="py-0"
273-
inputProps={{ 'data-hj-suppress': true }}
274-
/>
275-
</div>
326+
276327
</div>
277328
</div>
278329
</div>
@@ -291,6 +342,7 @@ AdminSearchForm.propTypes = {
291342
searchDateQuery: PropTypes.string,
292343
searchBudgetQuery: PropTypes.string,
293344
searchGroupQuery: PropTypes.string,
345+
searchEnrollmentQuery: PropTypes.string,
294346
}).isRequired,
295347
tableData: PropTypes.arrayOf(PropTypes.shape({})),
296348
budgets: PropTypes.arrayOf(PropTypes.shape({})),

src/components/AdminV2/index.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const Admin = ({
119119
}, [enterpriseId]);
120120

121121
const getMetadataForAction = (actionSlugParam) => {
122-
const expectedQueryParams = ['search', 'search_course', 'search_start_date', 'budget_uuid', 'group_uuid'];
122+
const expectedQueryParams = ['search', 'search_course', 'search_start_date', 'budget_uuid', 'group_uuid', 'search_enrollment'];
123123
const filteredQueryParams = getFilteredQueryParams(location.search, expectedQueryParams);
124124

125125
const defaultData = {
@@ -357,7 +357,7 @@ const Admin = ({
357357
const { search: searchQuery, pathname } = location;
358358
// remove the querys from the path
359359
const queryParams = new URLSearchParams(searchQuery);
360-
['search', 'search_course', 'search_start_date', 'budget_uuid', 'group_uuid'].forEach((searchTerm) => {
360+
['search', 'search_course', 'search_start_date', 'budget_uuid', 'group_uuid', 'search_enrollment'].forEach((searchTerm) => {
361361
queryParams.delete(searchTerm);
362362
});
363363
const resetQuery = queryParams.toString();
@@ -437,6 +437,7 @@ const Admin = ({
437437
searchDateQuery: queryParams.get('search_start_date') || '',
438438
searchBudgetQuery: queryParams.get('budget_uuid') || '',
439439
searchGroupQuery: queryParams.get('group_uuid') || '',
440+
searchEnrollmentQuery: queryParams.get('search_enrollment') || '',
440441
};
441442
const hasCompleteInsights = insights?.learner_engagement && insights?.learner_progress;
442443

src/components/AdminV2/tests/AdminSearchForm.test.jsx

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ describe('<AdminSearchForm />', () => {
5050
render(<AdminSearchFormWrapper {...DEFAULT_PROPS} />);
5151

5252
const formControls = await screen.findAllByTestId('admin-search-form-control');
53-
expect(formControls.length).toBe(2); // dropdowns
53+
expect(formControls.length).toBe(3); // dropdowns
5454
const searchBar = await screen.findByTestId('admin-search-bar'); // search input
5555
expect(searchBar).toBeInTheDocument();
56-
expect(formControls[1].textContent).toContain('Choose a course');
56+
expect(formControls[2].textContent).toContain('Choose a course');
5757
});
5858

5959
it.each([
@@ -69,6 +69,7 @@ describe('<AdminSearchForm />', () => {
6969
searchDateQuery: '',
7070
searchBudgetQuery: '',
7171
searchGroupQuery: '',
72+
searchEnrollmentQuery: '',
7273
};
7374

7475
const { rerender } = render(
@@ -196,6 +197,101 @@ describe('<AdminSearchForm />', () => {
196197
},
197198
);
198199
});
200+
it('selects the correct enrollment status', async () => {
201+
const user = userEvent.setup();
202+
203+
const props = {
204+
...DEFAULT_PROPS,
205+
location: { pathname: '/admin/learners' },
206+
};
207+
208+
render(<AdminSearchFormWrapper {...props} />);
209+
210+
const selectElement = screen.getByLabelText('Filter by enrollment');
211+
212+
// --- Test selecting "enrolled" ---
213+
await user.selectOptions(selectElement, 'enrolled');
214+
215+
expect(updateUrl).toHaveBeenCalledWith(
216+
undefined,
217+
'/admin/learners',
218+
{
219+
search_enrollment: 'enrolled',
220+
page: 1,
221+
},
222+
);
223+
224+
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
225+
props.enterpriseId,
226+
EVENT_NAMES.LEARNER_PROGRESS_REPORT.FILTER_BY_ENROLLMENT_DROPDOWN,
227+
{ enrollment: 'enrolled' },
228+
);
229+
230+
updateUrl.mockClear();
231+
sendEnterpriseTrackEvent.mockClear();
232+
233+
// --- Test selecting "unenrolled" ---
234+
await user.selectOptions(selectElement, 'unenrolled');
235+
236+
expect(updateUrl).toHaveBeenCalledWith(
237+
undefined,
238+
'/admin/learners',
239+
{
240+
search_enrollment: 'unenrolled',
241+
page: 1,
242+
},
243+
);
244+
245+
expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith(
246+
props.enterpriseId,
247+
EVENT_NAMES.LEARNER_PROGRESS_REPORT.FILTER_BY_ENROLLMENT_DROPDOWN,
248+
{ enrollment: 'unenrolled' },
249+
);
250+
});
251+
it('calls updateUrl when searching by email', async () => {
252+
const user = userEvent.setup();
253+
const props = {
254+
...DEFAULT_PROPS,
255+
location: { pathname: '/admin/learners' },
256+
};
257+
258+
render(<AdminSearchFormWrapper {...props} />);
259+
260+
const searchBar = screen.getByTestId('admin-search-bar');
261+
const input = searchBar.querySelector('input');
262+
263+
await user.type(input, 'test@example.com');
264+
await user.keyboard('{Enter}');
265+
266+
expect(updateUrl).toHaveBeenCalledWith(
267+
undefined,
268+
'/admin/learners',
269+
{ search: 'test@example.com', page: 1 },
270+
);
271+
});
272+
273+
it('calls updateUrl when clearing search', async () => {
274+
const user = userEvent.setup();
275+
const props = {
276+
...DEFAULT_PROPS,
277+
location: { pathname: '/admin/learners' },
278+
searchParams: { searchQuery: 'test@example.com' },
279+
};
280+
281+
render(<AdminSearchFormWrapper {...props} />);
282+
283+
const searchBar = screen.getByTestId('admin-search-bar');
284+
const clearButton = searchBar.querySelector('button[aria-label*="clear" i], button[title*="clear" i]')
285+
|| screen.getByRole('button', { name: /clear/i });
286+
287+
await user.click(clearButton);
288+
289+
expect(updateUrl).toHaveBeenCalledWith(
290+
undefined,
291+
'/admin/learners',
292+
{ search: undefined },
293+
);
294+
});
199295
});
200296

201297
describe('<AdminSearchForm />', () => {

src/eventTracking.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const PEOPLE_MANAGEMENT_EVENTS = {
3838
// learner-progress-report
3939
const LEARNER_PROGRESS_REPORT_EVENTS = {
4040
FILTER_BY_GROUP_DROPDOWN: `${LEARNER_PROGRESS_REPORT_PREFIX}.group_filter.clicked`,
41+
FILTER_BY_ENROLLMENT_DROPDOWN: `${LEARNER_PROGRESS_REPORT_PREFIX}.enrollment_filter.clicked`,
4142
};
4243
// analytics-v2
4344
const ANALYTICS_V2_EVENTS = {

src/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ const getPageOptionsFromUrl = () => {
186186
if (query.has('search_start_date')) {
187187
pageOptions.search_start_date = query.get('search_start_date');
188188
}
189+
if (query.has('search_enrollment')) {
190+
pageOptions.search_enrollment = query.get('search_enrollment');
191+
}
189192
return pageOptions;
190193
};
191194

0 commit comments

Comments
 (0)