Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a widget for managing thumbnail #1568

Merged
merged 4 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Add a widget for the instructor to upload thumbnail

### Fixed

- A thumbnail resource can be deleted and recreated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jest.mock('data/appData', () => ({
timed_text_tracks: [],
upload_state: 'processing',
},
static: {
img: {
liveBackground: 'path/to/image.png',
},
},
},
getDecodedJwt: () => ({
maintenance: false,
Expand Down
9 changes: 8 additions & 1 deletion src/frontend/components/DashboardVideoLive/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ import { DashboardVideoLive } from '.';

jest.mock('jwt-decode', () => jest.fn());
jest.mock('data/appData', () => ({
appData: { jwt: 'cool_token_m8' },
appData: {
jwt: 'cool_token_m8',
static: {
img: {
liveBackground: 'path/to/image.png',
},
},
},
getDecodedJwt: () => ({
permissions: {
can_access_dashboard: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { wrapInIntlProvider } from 'utils/tests/intl';
import { DashboardVideoLiveConfirmationModal } from '.';

const mockModalOnCloseOrCancel = jest.fn();
const mockModalConfirm = jest.fn();
const genericTitle = 'A generic title';
const genericContent =
'A generic content, which has for purpose to represent an example of an average string used in this modal. It has too be not too short, but also not too long.';

describe('<DashboardVideoLiveConfirmationModal />', () => {
beforeEach(() => {
/*
make sure to remove all body children, grommet layer gets rendered twice, known issue
https://github.com/grommet/grommet/issues/5200
*/
document.body.innerHTML = '';
document.body.appendChild(document.createElement('div'));
jest.resetAllMocks();
});

it('renders the modal and closes it with esc key', () => {
render(
wrapInIntlProvider(
<DashboardVideoLiveConfirmationModal
text={genericContent}
title={genericTitle}
onModalCloseOrCancel={mockModalOnCloseOrCancel}
onModalConfirm={mockModalConfirm}
/>,
),
);
screen.getByText(genericTitle);
screen.getByText(genericContent);
screen.getByRole('button', { name: '' });
screen.getByRole('button', { name: 'Confirm' });
screen.getByRole('button', { name: 'Cancel' });
act(() => {
userEvent.keyboard('{esc}');
});
expect(mockModalOnCloseOrCancel).toHaveBeenCalledTimes(1);
expect(mockModalConfirm).not.toHaveBeenCalled();
});

it('renders the modal and closes it with close button', () => {
render(
wrapInIntlProvider(
<DashboardVideoLiveConfirmationModal
text={genericContent}
title={genericTitle}
onModalCloseOrCancel={mockModalOnCloseOrCancel}
onModalConfirm={mockModalConfirm}
/>,
),
);
screen.getByRole('button', { name: 'Confirm' });
screen.getByRole('button', { name: 'Cancel' });
const closeButton = screen.getByRole('button', { name: '' });
act(() => {
userEvent.click(closeButton);
});
expect(mockModalOnCloseOrCancel).toHaveBeenCalledTimes(1);
expect(mockModalConfirm).not.toHaveBeenCalled();
});

it('renders the modal and closes it with cancel button', () => {
render(
wrapInIntlProvider(
<DashboardVideoLiveConfirmationModal
text={genericContent}
title={genericTitle}
onModalCloseOrCancel={mockModalOnCloseOrCancel}
onModalConfirm={mockModalConfirm}
/>,
),
);
screen.getByText(genericTitle);
screen.getByText(genericContent);
screen.getByRole('button', { name: '' });
screen.getByRole('button', { name: 'Confirm' });
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
act(() => {
userEvent.click(cancelButton);
});
expect(mockModalOnCloseOrCancel).toHaveBeenCalledTimes(1);
expect(mockModalConfirm).not.toHaveBeenCalled();
});

it('renders the modal and clicks on confirm button', () => {
render(
wrapInIntlProvider(
<DashboardVideoLiveConfirmationModal
text={genericContent}
title={genericTitle}
onModalCloseOrCancel={mockModalOnCloseOrCancel}
onModalConfirm={mockModalConfirm}
/>,
),
);
screen.getByText(genericTitle);
screen.getByText(genericContent);
screen.getByRole('button', { name: '' });
screen.getByRole('button', { name: 'Cancel' });
const confirmButton = screen.getByRole('button', { name: 'Confirm' });
act(() => {
userEvent.click(confirmButton);
});
expect(mockModalOnCloseOrCancel).not.toHaveBeenCalled();
expect(mockModalConfirm).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Box, Button, Layer, ResponsiveContext, Text } from 'grommet';
import { normalizeColor } from 'grommet/utils';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import styled from 'styled-components';

import { RoundCrossSVG } from 'components/SVGIcons/RoundCrossSVG';
import { theme } from 'utils/theme/theme';

const messages = defineMessages({
confirmButtonLabel: {
defaultMessage: 'Confirm',
description: 'Label of the confirming button',
id: 'components.DashboardVideoLiveConfirmationModal.confirmButtonLabel',
},
cancelButtonLabel: {
defaultMessage: 'Cancel',
description: 'Label of the cancelling button',
id: 'components.DashboardVideoLiveConfirmationModal.cancelButtonLabel',
},
});

const StyledTitleText = styled(Text)`
font-family: 'Roboto-Medium';
`;

const StyledText = styled(Text)`
line-height: 20px;
`;

interface DashboardVideoLiveConfirmationModalProps {
text: string;
title: string;
onModalCloseOrCancel: () => void;
onModalConfirm: () => void;
}

export const DashboardVideoLiveConfirmationModal = ({
text,
title,
onModalCloseOrCancel,
onModalConfirm,
}: DashboardVideoLiveConfirmationModalProps) => {
const intl = useIntl();
const size = React.useContext(ResponsiveContext);

return (
<Layer
onEsc={onModalCloseOrCancel}
onClickOutside={onModalCloseOrCancel}
responsive={false}
style={{
width: size === 'small' ? '95%' : '500px',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's huge difference. Are you sure about this values ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For info, this component comes from the PR on the shared live media : https://github.com/openfun/marsha/blob/9f991d654dfb754566af8ca8dd71c93ec49becd7/src/frontend/components/DashboardVideoLiveControlPane/customs/DashboardVideoLiveConfirmationModal/index.tsx

I think the values are correct because this is what I wanted : a almost full page modal when the screen is small, a medium fixed sized modal otherwise

border: `1px solid ${normalizeColor('blue-active', theme)}`,
}}
>
<Box background="bg-info" direction="column" round="6px">
<Box
direction="row-reverse"
pad={{ horizontal: 'small', top: 'small' }}
>
<Button
onClick={onModalCloseOrCancel}
plain
style={{ display: 'block', padding: 0 }}
>
<RoundCrossSVG height="20px" iconColor="blue-active" width="20px" />
</Button>
</Box>
<Box
direction="column"
gap="medium"
pad={{ horizontal: 'large', bottom: '30px' }}
>
<StyledTitleText color="blue-active" size="1.5rem" truncate>
{title}
</StyledTitleText>
<StyledText color="blue-active" size="1rem">
{text}
</StyledText>
<Box direction="row" gap="medium">
<Button
primary
label={intl.formatMessage(messages.confirmButtonLabel)}
onClick={onModalConfirm}
/>
<Button
secondary
label={intl.formatMessage(messages.cancelButtonLabel)}
onClick={onModalCloseOrCancel}
style={{
color: normalizeColor('blue-active', theme),
}}
/>
</Box>
</Box>
</Box>
</Layer>
);
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { within } from '@testing-library/dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import faker from 'faker';
import { ResponsiveContext } from 'grommet';
import { DateTime } from 'luxon';
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';

import { useThumbnail } from 'data/stores/useThumbnail';
import { JoinMode } from 'types/tracks';
import { videoMockFactory } from 'utils/tests/factories';
import { thumbnailMockFactory, videoMockFactory } from 'utils/tests/factories';
import { wrapInIntlProvider } from 'utils/tests/intl';
import { DashboardVideoLiveControlPane } from './index';
import { DashboardVideoLiveControlPane } from '.';

jest.mock('data/appData', () => ({
appData: {},
appData: {
static: {
img: {
liveBackground: 'path/to/image',
},
},
},
}));

const currentDate = DateTime.fromISO('2022-01-13T12:00');
Expand All @@ -26,16 +34,25 @@ describe('<DashboardVideoLiveControlPane />', () => {
jest.useRealTimers();
});
it('renders DashboardVideoLiveControlPane', () => {
const videoId = faker.datatype.uuid();
const mockedThumbnail = thumbnailMockFactory({
video: videoId,
is_ready_to_show: true,
});
const mockVideo = videoMockFactory({
id: videoId,
kernicPanel marked this conversation as resolved.
Show resolved Hide resolved
title: 'An example title',
allow_recording: false,
is_public: true,
join_mode: JoinMode.APPROVAL,
starting_at: currentDate.toString(),
estimated_duration: '00:30',
description: 'An example description',
thumbnail: mockedThumbnail,
});

useThumbnail.getState().addResource(mockedThumbnail);

const queryClient = new QueryClient();

render(
Expand Down Expand Up @@ -115,5 +132,17 @@ describe('<DashboardVideoLiveControlPane />', () => {
});
const select = within(button).getByRole('textbox');
expect(select).toHaveValue('Accept joining the discussion after approval');

// DashboardVideoLiveWidgetThumbnail
screen.getByText('Thumbnail');
const img = screen.getByRole('img', { name: 'Live video thumbnail' });
expect(img.getAttribute('src')).toEqual(
'https://example.com/default_thumbnail/144',
);
expect(img.getAttribute('srcset')).toEqual(
'https://example.com/default_thumbnail/144 256w, https://example.com/default_thumbnail/240 426w, https://example.com/default_thumbnail/480 854w, https://example.com/default_thumbnail/720 1280w, https://example.com/default_thumbnail/1080 1920w',
);
screen.getByRole('button', { name: 'Delete thumbnail' });
screen.getByRole('button', { name: 'Upload an image' });
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';

import { Video } from 'types/tracks';

import { DashboardVideoLiveWidgetsContainer } from './widgets/DashboardVideoLiveWidgetsContainer';
import { DashboardVideoLiveWidgetGeneralTitle } from './widgets/DashboardVideoLiveWidgetGeneralTitle';
import { DashboardVideoLiveWidgetToolsAndApplications } from './widgets/DashboardVideoLiveWidgetToolsAndApplications';
import { DashboardVideoLiveWidgetJoinMode } from './widgets/DashboardVideoLiveWidgetJoinMode';
import { DashboardVideoLiveWidgetLivePairing } from './widgets/DashboardVideoLiveWidgetLivePairing';
import { DashboardVideoLiveWidgetSchedulingAndDescription } from './widgets/DashboardVideoLiveWidgetSchedulingAndDescription';
import { DashboardVideoLiveWidgetThumbnail } from './widgets/DashboardVideoLiveWidgetThumbnail';
import { DashboardVideoLiveWidgetToolsAndApplications } from './widgets/DashboardVideoLiveWidgetToolsAndApplications';
import { DashboardVideoLiveWidgetVisibilityAndInteraction } from './widgets/DashboardVideoLiveWidgetVisibilityAndInteraction';
import { DashboardVideoLiveWidgetVOD } from './widgets/DashboardVideoLiveWidgetVOD';

Expand All @@ -27,6 +27,7 @@ export const DashboardVideoLiveControlPane = ({
<DashboardVideoLiveWidgetLivePairing video={video} />
<DashboardVideoLiveWidgetVOD video={video} />
<DashboardVideoLiveWidgetJoinMode video={video} />
<DashboardVideoLiveWidgetThumbnail />
</DashboardVideoLiveWidgetsContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/react';
import React from 'react';

import { thumbnailMockFactory } from 'utils/tests/factories';
import { wrapInIntlProvider } from 'utils/tests/intl';
import { ThumbnailDisplayer } from '.';

jest.mock('data/appData', () => ({
appData: {},
}));

describe('<ThumbnailDisplayer />', () => {
it('renders ThumbnailDisplayer', () => {
const mockedThumbnail = thumbnailMockFactory();
render(
wrapInIntlProvider(
<ThumbnailDisplayer urlsThumbnail={mockedThumbnail.urls} />,
),
);

const img = screen.getByRole('img', { name: 'Live video thumbnail' });
expect(img.getAttribute('src')).toEqual(
'https://example.com/default_thumbnail/144',
);
expect(img.getAttribute('srcset')).toEqual(
'https://example.com/default_thumbnail/144 256w, https://example.com/default_thumbnail/240 426w, https://example.com/default_thumbnail/480 854w, https://example.com/default_thumbnail/720 1280w, https://example.com/default_thumbnail/1080 1920w',
);
});
});
Loading