Skip to content

Commit

Permalink
feat: availability tab with calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
01001110J committed Dec 18, 2024
1 parent a8bc418 commit 0cac229
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 40 deletions.
2 changes: 2 additions & 0 deletions src/features/Dashboard/WeeklySchedule/__test__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jest.mock('date-fns', () => ({
endOfWeek: jest.fn(() => null),
isWithinInterval: jest.fn(() => true),
format: jest.fn(() => 'Jan 23, 2024'),
startOfMonth: jest.fn(() => new Date('2024-12-01T00:00:00.000Z')),
endOfMonth: jest.fn(() => new Date('2024-12-31T23:59:59.999Z')),
}));

describe('WeeklySchedule component', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React from 'react';
import { waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { renderWithProviders } from 'test-utils';
import { fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter, Route } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import '@testing-library/jest-dom/extend-expect';

import { renderWithProviders } from 'test-utils';
import InstructorsDetailPage from 'features/Instructors/InstructorsDetailPage';

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));

const mockStore = {
main: {
selectedInstitution: {
Expand Down Expand Up @@ -69,6 +74,20 @@ const mockStore = {
num_pages: 1,
current_page: 1,
},
events: {
data: [
{
id: 1,
title: 'Not available',
start: '2024-09-04T00:00:00Z',
end: '2024-09-13T00:00:00Z',
type: 'virtual',
},
],
count: 1,
num_pages: 1,
current_page: 1,
},
},
};

Expand Down Expand Up @@ -98,4 +117,47 @@ describe('InstructorsDetailPage', () => {
expect(component.container).toHaveTextContent('in progress');
});
});

test('Should render the calendar if the flag is provided', async () => {
getConfig.mockImplementation(() => ({ enable_instructor_calendar: true }));

const { getByText } = renderWithProviders(
<MemoryRouter initialEntries={['/instructors/instructor']}>
<Route path="/instructors/:instructorUsername">
<InstructorsDetailPage />
</Route>
</MemoryRouter>,
{ preloadedState: mockStore },
);

fireEvent.click(getByText('Availability'));

await waitFor(() => {
expect(getByText('Today')).toBeInTheDocument();
expect(getByText('Sunday')).toBeInTheDocument();
expect(getByText('Monday')).toBeInTheDocument();
expect(getByText('Tuesday')).toBeInTheDocument();
expect(getByText('Wednesday')).toBeInTheDocument();
expect(getByText('Thursday')).toBeInTheDocument();
expect(getByText('Friday')).toBeInTheDocument();
expect(getByText('Saturday')).toBeInTheDocument();
});
});

test('Should not render the calendar if the flag is false or is not provided', async () => {
getConfig.mockImplementation(() => ({ enable_instructor_calendar: false }));

const { queryByText } = renderWithProviders(
<MemoryRouter initialEntries={['/instructors/instructor']}>
<Route path="/instructors/:instructorUsername">
<InstructorsDetailPage />
</Route>
</MemoryRouter>,
{ preloadedState: mockStore },
);

const calendarTab = queryByText('Availability');

expect(calendarTab).not.toBeInTheDocument();
});
});
135 changes: 101 additions & 34 deletions src/features/Instructors/InstructorsDetailPage/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useState, useEffect, useRef } from 'react';
import React, {
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import {
Expand All @@ -7,56 +12,91 @@ import {
Tabs,
Tab,
} from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { CalendarExpanded } from 'react-paragon-topaz';
import { startOfMonth, endOfMonth } from 'date-fns';

import Table from 'features/Main/Table';
import { fetchClassesData } from 'features/Classes/data/thunks';
import { resetClassesTable, updateCurrentPage } from 'features/Classes/data/slice';
import { fetchInstructorsData } from 'features/Instructors/data/thunks';
import { fetchInstructorsData, fetchEventsData, resetEvents } from 'features/Instructors/data';
import { columns } from 'features/Instructors/InstructorDetailTable/columns';
import { initialPage, RequestStatus } from 'features/constants';

import { useInstitutionIdQueryParam } from 'hooks';

import LinkWithQuery from 'features/Main/LinkWithQuery';

import 'features/Instructors/InstructorsDetailPage/index.scss';

const initialState = {
instructor_id: null,
start_date: startOfMonth(new Date()).toISOString(),
end_date: endOfMonth(new Date()).toISOString(),
};

const defaultInstructorInfo = {
instructorUsername: '',
instructorName: '',
};

const InstructorsDetailPage = () => {
const history = useHistory();
const dispatch = useDispatch();
const { instructorUsername } = useParams();
const addQueryParam = useInstitutionIdQueryParam();
const events = useSelector((state) => state.instructors.events.data);

const institutionRef = useRef(undefined);
const [currentPage, setCurrentPage] = useState(initialPage);
const [eventsList, setEventsList] = useState([]);
const [rangeDates, setRangeDates] = useState(initialState);

const defaultInstructorInfo = {
instructorUsername: '',
instructorName: '',
};
const getRangeDate = useCallback((range) => {
setRangeDates({
start_date: range.start.toISOString(),
end_date: range.end.toISOString(),
});
}, [setRangeDates]);

const institution = useSelector((state) => state.main.selectedInstitution);
const classes = useSelector((state) => state.classes.table);
const instructorInfo = useSelector((state) => state.instructors.table.data)
.find((instructor) => instructor?.instructorUsername === instructorUsername) || defaultInstructorInfo;
?.find((instructor) => instructor?.instructorUsername === instructorUsername) || defaultInstructorInfo;

const isLoading = classes.status === RequestStatus.LOADING;
const showInstructorCalendar = getConfig()?.enable_instructor_calendar || false;

const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
dispatch(updateCurrentPage(targetPage));
};

useEffect(() => {
if (institution.id && instructorUsername) {
dispatch(fetchClassesData(institution.id, initialPage, '', { instructor: instructorUsername }));
if (institution.id) {
dispatch(fetchInstructorsData(institution.id, initialPage, { instructor: instructorUsername }));
}
}, [dispatch, institution.id, instructorUsername]);

useEffect(() => {
if (institution.id) {
dispatch(fetchClassesData(institution.id, currentPage, '', { instructor: instructorUsername }));
}

return () => {
dispatch(resetClassesTable());
};
}, [dispatch, institution.id, instructorUsername]);
}, [dispatch, institution.id, currentPage, instructorUsername]);

useEffect(() => {
dispatch(fetchClassesData(institution.id, currentPage, '', { instructor: instructorUsername }));
}, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps
if (instructorInfo.instructorId && showInstructorCalendar) {
dispatch(fetchEventsData({ ...rangeDates }));
}

return () => {
dispatch(resetEvents());
};
}, [dispatch, instructorInfo.instructorId, rangeDates, showInstructorCalendar]);

useEffect(() => {
if (institution.id !== undefined && institutionRef.current === undefined) {
Expand All @@ -66,8 +106,19 @@ const InstructorsDetailPage = () => {
if (institution.id !== institutionRef.current) {
history.push(addQueryParam('/instructors'));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [institution, history]);
}, [institution, history, addQueryParam]);

useEffect(() => {
if (events.length > 0) {
const list = events?.map(event => ({
...event,
start: new Date(event.start),
end: new Date(event.end),
}));

setEventsList(list);
}
}, [events]);

return (
<Container size="xl" className="px-4 mt-3">
Expand All @@ -81,27 +132,43 @@ const InstructorsDetailPage = () => {
</div>

<Tabs variant="tabs" defaultActiveKey="classes" id="uncontrolled-tab-example" className="mb-3 tabstpz">
<Tab eventKey="classes" title="Classes" />
<Tab eventKey="classes" title="Classes" tabClassName="text-decoration-none">
<Table
isLoading={isLoading}
columns={columns}
count={classes.count}
data={classes.data}
text="No classes found."
/>
{classes.numPages > initialPage && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={classes.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
className="mx-auto pagination-table"
size="small"
/>
)}
</Tab>
{
showInstructorCalendar && (
<Tab eventKey="availability" title="Availability" tabClassName="text-decoration-none">
<div className="p-3 bg-white mb-5 rounded-bottom container-calendar">
<CalendarExpanded
eventsList={eventsList}
onRangeChange={getRangeDate}
onEdit={() => {}}
onDelete={() => {}}
onDeleteMultiple={() => {}}
onEditSinglRec={() => {}}
/>
</div>
</Tab>
)
}
</Tabs>

<Table
isLoading={isLoading}
columns={columns}
count={classes.count}
data={classes.data}
text="No classes found."
/>
{classes.numPages > initialPage && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={classes.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
className="mx-auto pagination-table"
size="small"
/>
)}
</Container>
);
};
Expand Down
5 changes: 5 additions & 0 deletions src/features/Instructors/InstructorsDetailPage/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'assets/colors.scss';

.container-calendar {
box-shadow: 0px 3px 12px 0px $gray-30;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
handleInstructorsEnrollment,
handleNewInstructor,
getEventsByInstructor,
} from 'features/Instructors/data/api';

jest.mock('@edx/frontend-platform/auth', () => ({
Expand Down Expand Up @@ -66,4 +67,37 @@ describe('should call getAuthenticatedHttpClient with the correct parameters', (
+ '?institution_id=1&instructor_email=instructor%40example.com&first_name=Sam&last_name=F',
);
});

test('getEventsByInstructor', () => {
const httpClientMock = {
get: jest.fn(),
};

const eventRequestInfo = {
instructor_id: '3',
start_date: '2024-12-01T05:00:00.000Z',
end_date: '2025-01-01T04:59:59.999Z',
page: '1',
};

getAuthenticatedHttpClient.mockReturnValue(httpClientMock);

getEventsByInstructor({ ...eventRequestInfo });

expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(1);
expect(getAuthenticatedHttpClient).toHaveBeenCalledWith();

expect(httpClientMock.get).toHaveBeenCalledTimes(1);
expect(httpClientMock.get).toHaveBeenCalledWith(
'http://localhost:18000/pearson_course_operation/api/v2/events/',
{
params: {
instructor_id: '3',
start_date: '2024-12-01T05:00:00.000Z',
end_date: '2025-01-01T04:59:59.999Z',
page: '1',
},
},
);
});
});
15 changes: 15 additions & 0 deletions src/features/Instructors/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,22 @@ function handleNewInstructor(institutionId, instructorFormData) {
return getAuthenticatedHttpClient().post(`${apiV2BaseUrl}&${searchParams}`);
}

/**
* Get events list by instructor.
*
* @param {object} - An object with the start and end date range for the calendar
* Dates in format ISO
* @returns {Promise} - A promise that resolves with the response of the GET request.
*/
function getEventsByInstructor(params) {
return getAuthenticatedHttpClient().get(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/events/`,
{ params },
);
}

export {
handleInstructorsEnrollment,
handleNewInstructor,
getEventsByInstructor,
};
7 changes: 5 additions & 2 deletions src/features/Instructors/data/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { reducer } from 'features/Instructors/data/slice';
export { reducer, resetEvents } from 'features/Instructors/data/slice';
export {
fetchInstructorsData, assignInstructors, fetchInstructorsOptionsData,
fetchInstructorsData,
assignInstructors,
fetchInstructorsOptionsData,
fetchEventsData,
} from 'features/Instructors/data/thunks';
Loading

0 comments on commit 0cac229

Please sign in to comment.