From 463edc6493346376cf1cf2735f9d95834252d2b7 Mon Sep 17 00:00:00 2001 From: Henri BAUDESSON Date: Tue, 18 Jul 2023 17:08:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(front)=20add=20retention=20date=20wid?= =?UTF-8?q?gets=20to=20classrooms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the possibility to add / edit the retention date on classrooms through a widget --- .../ClassroomWidgetProvider/index.tsx | 7 + .../widgets/RetentionDate/index.spec.tsx | 129 ++++++++++++++++++ .../widgets/RetentionDate/index.tsx | 114 ++++++++++++++++ .../src/utils/tests/factories.ts | 1 + .../src/types/apps/classroom/models.ts | 1 + 5 files changed, 252 insertions(+) create mode 100644 src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.spec.tsx create mode 100644 src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.tsx diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/index.tsx index b14505cdcc..7798d93df0 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/index.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/index.tsx @@ -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'; @@ -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 } = { @@ -50,6 +52,10 @@ const widgetLoader: { [key in WidgetType]: WidgetProps } = { component: , size: WidgetSize.DEFAULT, }, + [WidgetType.RETENTION_DATE]: { + component: , + size: WidgetSize.DEFAULT, + }, [WidgetType.DELETE_CLASSROOM]: { component: , size: WidgetSize.DEFAULT, @@ -63,6 +69,7 @@ const classroomWidgets: WidgetType[] = [ WidgetType.SCHEDULING, WidgetType.SUPPORT_SHARING, WidgetType.RECORDINGS, + WidgetType.RETENTION_DATE, WidgetType.DELETE_CLASSROOM, ]; diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.spec.tsx new file mode 100644 index 0000000000..4c7a397001 --- /dev/null +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.spec.tsx @@ -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 ', () => { + 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( + + + , + 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( + + + , + 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( + + + , + 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!'); + }); +}); diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.tsx new file mode 100644 index 0000000000..3fdf619df5 --- /dev/null +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/RetentionDate/index.tsx @@ -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 + >(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( + (updatedClassroomProperty: Partial) => { + 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 ( + + + onChange(value)} + /> + { + onChange(''); + }} + /> + + + ); +}; diff --git a/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts b/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts index 7c2abe7c8a..b5118b6aef 100644 --- a/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts +++ b/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts @@ -29,6 +29,7 @@ export const classroomMockFactory = >( estimated_duration: null, public_token: null, instructor_token: null, + retention_date: null, recordings: [], enable_waiting_room: false, enable_shared_notes: true, diff --git a/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts b/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts index b1e8970236..c95235b18e 100644 --- a/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts +++ b/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts @@ -28,6 +28,7 @@ export interface Classroom extends Resource { enable_presentation_supports: boolean; enable_recordings: boolean; recording_purpose: Nullable; + retention_date: Nullable; vod_conversion_enabled: boolean; }