Skip to content

Commit

Permalink
✨(front) add retention date widgets to classrooms
Browse files Browse the repository at this point in the history
Add the possibility to add / edit the retention date on
classrooms through a widget
  • Loading branch information
polyhb committed Jul 18, 2023
1 parent 64c58c7 commit 463edc6
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DeleteClassroom } from './widgets/DeleteClassroom';
import { Description } from './widgets/Description';
import { Invite } from './widgets/Invite';
import { Recordings } from './widgets/Recordings';
import { RetentionDate } from './widgets/RetentionDate';
import { Scheduling } from './widgets/Scheduling';
import { SupportSharing } from './widgets/SupportSharing';
import { ToolsAndApplications } from './widgets/ToolsAndApplications';
Expand All @@ -23,6 +24,7 @@ enum WidgetType {
SUPPORT_SHARING = 'SUPPORT_SHARING',
RECORDINGS = 'RECORDINGS',
DELETE_CLASSROOM = 'DELETE_CLASSROOM',
RETENTION_DATE = 'RETENTION_DATE',
}

const widgetLoader: { [key in WidgetType]: WidgetProps } = {
Expand Down Expand Up @@ -50,6 +52,10 @@ const widgetLoader: { [key in WidgetType]: WidgetProps } = {
component: <ToolsAndApplications />,
size: WidgetSize.DEFAULT,
},
[WidgetType.RETENTION_DATE]: {
component: <RetentionDate />,
size: WidgetSize.DEFAULT,
},
[WidgetType.DELETE_CLASSROOM]: {
component: <DeleteClassroom />,
size: WidgetSize.DEFAULT,
Expand All @@ -63,6 +69,7 @@ const classroomWidgets: WidgetType[] = [
WidgetType.SCHEDULING,
WidgetType.SUPPORT_SHARING,
WidgetType.RECORDINGS,
WidgetType.RETENTION_DATE,
WidgetType.DELETE_CLASSROOM,
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { InfoWidgetModalProvider, useJwt } from 'lib-components';
import { render } from 'lib-tests';

import { classroomMockFactory } from '@lib-classroom/utils/tests/factories';
import { wrapInClassroom } from '@lib-classroom/utils/wrapInClassroom';

import { RetentionDate } from '.';

jest.mock('lib-components', () => ({
...jest.requireActual('lib-components'),
report: jest.fn(),
}));

describe('Classroom <RetentionDate />', () => {
beforeEach(() => {
useJwt.setState({
jwt: 'json web token',
});
});

afterEach(() => {
jest.resetAllMocks();
fetchMock.restore();
});

it('renders the component and set a date with success', async () => {
const mockedClassroom = classroomMockFactory();

fetchMock.mock(`/api/classrooms/${mockedClassroom.id}/`, 200, {
method: 'PATCH',
});

render(
wrapInClassroom(
<InfoWidgetModalProvider value={null}>
<RetentionDate />
</InfoWidgetModalProvider>,
mockedClassroom,
),
);

expect(screen.getByText('Retention date')).toBeInTheDocument();
const datePickerInput = screen.getByRole('textbox');

fireEvent.change(datePickerInput, {
target: { value: '2020/01/01' },
});

expect((datePickerInput as HTMLInputElement).value).toBe('2020/01/01');

await waitFor(() => expect(fetchMock.calls()).toHaveLength(1));

const lastCall = fetchMock.lastCall();
expect(lastCall).not.toBe(undefined);
expect(lastCall?.[0]).toBe(`/api/classrooms/${mockedClassroom.id}/`);
expect(lastCall?.[1]?.body).toEqual('{"retention_date":"2020-01-01"}');
expect(lastCall?.[1]?.method).toBe('PATCH');
});

it('renders the component with a default date and deletes it', async () => {
const mockedClassroom = classroomMockFactory({
retention_date: '2020-01-01',
});

fetchMock.mock(`/api/classrooms/${mockedClassroom.id}/`, 200, {
method: 'PATCH',
});

render(
wrapInClassroom(
<InfoWidgetModalProvider value={null}>
<RetentionDate />
</InfoWidgetModalProvider>,
mockedClassroom,
),
);

expect(screen.getByText('Retention date')).toBeInTheDocument();
const datePickerInput = await screen.findByRole('textbox');

expect((datePickerInput as HTMLInputElement).value).toBe('2020/01/01');

const deleteButton = await screen.findByRole('button', {
name: 'Delete retention date',
});

await userEvent.click(deleteButton);

await waitFor(() => expect(fetchMock.calls()).toHaveLength(1));

const lastCall = fetchMock.lastCall();
expect(lastCall).not.toBe(undefined);
expect(lastCall?.[0]).toBe(`/api/classrooms/${mockedClassroom.id}/`);
expect(lastCall?.[1]?.body).toEqual('{"retention_date":null}');
expect(lastCall?.[1]?.method).toBe('PATCH');
});

it('fails to update the video and displays the right error message', async () => {
// Set by default with an All rights reserved license
const mockedClassroom = classroomMockFactory({
retention_date: '2020-01-01',
});
fetchMock.patch(`/api/classrooms/${mockedClassroom.id}/`, 401);

render(
wrapInClassroom(
<InfoWidgetModalProvider value={null}>
<RetentionDate />
</InfoWidgetModalProvider>,
mockedClassroom,
),
);

expect(screen.getByText('Retention date')).toBeInTheDocument();

const deleteButton = await screen.findByRole('button', {
name: 'Delete retention date',
});

await userEvent.click(deleteButton);

await waitFor(() => expect(fetchMock.calls()).toHaveLength(1));

await screen.findByText('Classroom update has failed!');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Box, Button, DateInput } from 'grommet';
import { Nullable } from 'lib-common';
import { Classroom, FoldableItem, debounce } from 'lib-components';
import { DateTime } from 'luxon';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { defineMessages, useIntl } from 'react-intl';
import styled from 'styled-components';

import { useUpdateClassroom } from '@lib-classroom/data/queries';
import { useCurrentClassroom } from '@lib-classroom/hooks/useCurrentClassroom';

const messages = defineMessages({
info: {
defaultMessage:
'This widget allows you to change the retention date of the classroom. Once this date is reached, the classroom will be deleted.',
description: 'Info of the widget used to change classroom retention date.',
id: 'components.ClassroomRetentionDate.info',
},
title: {
defaultMessage: 'Retention date',
description: 'Title of the widget used to change classroom retention date.',
id: 'components.ClassroomRetentionDate.title',
},
updateClassroomSuccess: {
defaultMessage: 'Classroom updated.',
description: 'Message displayed when classroom is successfully updated.',
id: 'component.ClassroomRetentionDate.updateVideoSuccess',
},
updateClassroomFail: {
defaultMessage: 'Classroom update has failed!',
description: 'Message displayed when classroom update has failed.',
id: 'component.ClassroomRetentionDate.updateVideoFail',
},
deleteClassroomRetentionDateButton: {
defaultMessage: 'Delete retention date',
description: 'Button used to delete classroom retention date.',
id: 'component.ClassroomRetentionDate.deleteClassroomRetentionDateButton',
},
});

const StyledAnchorButton = styled(Button)`
height: 50px;
font-family: 'Roboto-Medium';
display: flex;
align-items: center;
justify-content: center;
`;

export const RetentionDate = () => {
const intl = useIntl();
const classroom = useCurrentClassroom();
const [selectedRetentionDate, setSelectedRetentionDate] = useState<
Nullable<string>
>(classroom.retention_date);

const updateClassroomMutation = useUpdateClassroom(classroom.id, {
onSuccess: () => {
toast.success(intl.formatMessage(messages.updateClassroomSuccess));
},
onError: () => {
toast.error(intl.formatMessage(messages.updateClassroomFail));
},
});
const debouncedUpdatedClassroom = debounce<Classroom>(
(updatedClassroomProperty: Partial<Classroom>) => {
updateClassroomMutation.mutate(updatedClassroomProperty);
},
);

function onChange(new_retention_date: string | string[]) {
let new_retention_date_formatted = null;
if (new_retention_date && typeof new_retention_date === 'string') {
const utcDateTime = DateTime.fromISO(new_retention_date, { zone: 'utc' });
const localDateTime = utcDateTime.toLocal();
new_retention_date_formatted = localDateTime.toFormat('yyyy-MM-dd');
}
debouncedUpdatedClassroom({ retention_date: new_retention_date_formatted });
setSelectedRetentionDate(new_retention_date_formatted);
}

return (
<FoldableItem
infoText={intl.formatMessage(messages.info)}
initialOpenValue
title={intl.formatMessage(messages.title)}
>
<Box direction="column" gap="small" style={{ marginTop: '0.75rem' }}>
<DateInput
format={intl.locale === 'fr' ? 'dd/mm/yyyy' : 'yyyy/mm/dd'}
value={selectedRetentionDate || ''}
onChange={({ value }) => onChange(value)}
/>
<StyledAnchorButton
disabled={!selectedRetentionDate}
a11yTitle={intl.formatMessage(
messages.deleteClassroomRetentionDateButton,
)}
fill="horizontal"
label={intl.formatMessage(
messages.deleteClassroomRetentionDateButton,
)}
primary
title={intl.formatMessage(
messages.deleteClassroomRetentionDateButton,
)}
onClick={() => {
onChange('');
}}
/>
</Box>
</FoldableItem>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const classroomMockFactory = <T extends Partial<Classroom>>(
estimated_duration: null,
public_token: null,
instructor_token: null,
retention_date: null,
recordings: [],
enable_waiting_room: false,
enable_shared_notes: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Classroom extends Resource {
enable_presentation_supports: boolean;
enable_recordings: boolean;
recording_purpose: Nullable<string>;
retention_date: Nullable<string>;
vod_conversion_enabled: boolean;
}

Expand Down

0 comments on commit 463edc6

Please # to comment.