feat: add notification of transcription error (#715)

This commit is contained in:
Kristin Aoki
2023-11-28 16:55:00 -05:00
committed by GitHub
parent 2402769d9d
commit a2dceac62f
13 changed files with 229 additions and 106 deletions

View File

@@ -1,19 +1,24 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
injectIntl,
intlShape,
FormattedMessage,
} from '@edx/frontend-platform/i18n';
import {
Icon,
ModalDialog,
Stack,
Truncate,
} from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import messages from './messages';
import UsageMetricsMessages from './UsageMetricsMessage';
import FileThumbnail from './ThumbnailPreview';
import { TRANSCRIPT_FAILURE_STATUSES } from '../videos-page/data/constants';
import AlertMessage from '../../generic/alert-message';
const InfoModal = ({
file,
@@ -23,55 +28,74 @@ const InfoModal = ({
usagePathStatus,
error,
sidebar,
}) => (
<ModalDialog
title={file?.displayName}
isOpen={isOpen}
onClose={onClose}
size="lg"
hasCloseButton
data-testid="file-info-modal"
style={{ minHeight: '799px' }}
>
<ModalDialog.Header>
<ModalDialog.Title>
<div style={{ wordBreak: 'break-word' }}>
<Truncate lines={2} className="font-weight-bold small mt-3">
{file?.displayName}
</Truncate>
</div>
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="pt-0 x-small">
<hr />
<div className="row flex-nowrap m-0 mt-4">
<div className="col-7 mr-3">
<Stack gap={5}>
<FileThumbnail
thumbnail={file?.thumbnail}
externalUrl={file?.externalUrl}
displayName={file?.displayName}
wrapperType={file?.wrapperType}
id={file?.id}
status={file?.status}
thumbnailPreview={thumbnailPreview}
imageSize={{ width: '503px', height: '281px' }}
/>
<div>
<div className="row m-0 font-weight-bold">
<FormattedMessage {...messages.usageTitle} />
// injected
intl,
}) => {
const [activeTab, setActiveTab] = useState('fileInfo');
const showTranscriptionError = TRANSCRIPT_FAILURE_STATUSES.includes(file?.transcriptionStatus)
&& activeTab !== 'fileInfo';
return (
<ModalDialog
title={file?.displayName}
isOpen={isOpen}
onClose={onClose}
size="lg"
hasCloseButton
data-testid="file-info-modal"
style={{ minHeight: '799px' }}
>
<ModalDialog.Header>
<ModalDialog.Title>
<div style={{ wordBreak: 'break-word' }}>
<Truncate lines={2} className="font-weight-bold small mt-3">
{file?.displayName}
</Truncate>
</div>
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="pt-0 x-small">
<hr />
{showTranscriptionError && (
<AlertMessage
description={(
<div className="row m-0 align-itmes-center">
<Icon src={Error} className="text-danger-500 mr-2" />
{intl.formatMessage(messages.transcriptionErrorMessage, { error: file.errorDescription })}
</div>
<UsageMetricsMessages {...{ usageLocations: file?.usageLocations, usagePathStatus, error }} />
</div>
</Stack>
)}
variant="danger"
/>
)}
<div className="row flex-nowrap m-0 mt-4">
<div className="col-7 mr-3">
<Stack gap={5}>
<FileThumbnail
thumbnail={file?.thumbnail}
externalUrl={file?.externalUrl}
displayName={file?.displayName}
wrapperType={file?.wrapperType}
id={file?.id}
status={file?.status}
thumbnailPreview={thumbnailPreview}
imageSize={{ width: '503px', height: '281px' }}
/>
<div>
<div className="row m-0 font-weight-bold">
<FormattedMessage {...messages.usageTitle} />
</div>
<UsageMetricsMessages {...{ usageLocations: file?.usageLocations, usagePathStatus, error }} />
</div>
</Stack>
</div>
<div className="col-5">
{sidebar(file, activeTab, setActiveTab)}
</div>
</div>
<div className="col-5">
{sidebar(file)}
</div>
</div>
</ModalDialog.Body>
</ModalDialog>
);
</ModalDialog.Body>
</ModalDialog>
);
};
InfoModal.propTypes = {
file: PropTypes.shape({
@@ -86,6 +110,8 @@ InfoModal.propTypes = {
fileSize: PropTypes.number.isRequired,
usageLocations: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string,
transcriptionStatus: PropTypes.string,
errorDescription: PropTypes.string,
}),
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
@@ -93,6 +119,8 @@ InfoModal.propTypes = {
error: PropTypes.arrayOf(PropTypes.string).isRequired,
thumbnailPreview: PropTypes.func.isRequired,
sidebar: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
InfoModal.defaultProps = {

View File

@@ -6,6 +6,7 @@ import {
MoreInfoColumn,
StatusColumn,
ThumbnailColumn,
TranscriptColumn,
} from './table-components';
import FileInput, { useFileInput } from './FileInput';
@@ -19,6 +20,7 @@ export {
ThumbnailColumn,
FileInput,
useFileInput,
TranscriptColumn,
};
export { default as FileTable } from './FileTable';
export { default as EditFileErrors } from './EditFileErrors';

View File

@@ -37,6 +37,10 @@ const messages = defineMessages({
id: 'course-authoring.files-and-upload.errorAlert.message',
defaultMessage: '{message}',
},
transcriptionErrorMessage: {
id: 'course-authoring.files-and-uploads.file-info.transcripts.error.alert',
defaultMessage: 'Transcript failed: "{error}"',
},
usageTitle: {
id: 'course-authoring.files-and-uploads.file-info.usage.title',
defaultMessage: 'Usage',

View File

@@ -9,6 +9,7 @@ import {
MoreInfoColumn,
StatusColumn,
ThumbnailColumn,
TranscriptColumn,
} from './table-custom-columns';
export {
@@ -22,4 +23,5 @@ export {
MoreInfoColumn,
StatusColumn,
ThumbnailColumn,
TranscriptColumn,
};

View File

@@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { TRANSCRIPT_FAILURE_STATUSES } from '../../../videos-page/data/constants';
const TranscriptColumn = ({ row }) => {
const { transcripts, transcriptionStatus } = row.original;
const numOfTranscripts = transcripts?.length;
const transcriptMessage = numOfTranscripts > 0 ? `(${numOfTranscripts}) available` : null;
return (
<div className="row m-0 align-items-center">
{TRANSCRIPT_FAILURE_STATUSES.includes(transcriptionStatus) && (
<Icon src={Info} size="sm" className="mr-2 text-danger-500" />
)}
<FormattedMessage
id="course-authoring.videos-page.table.transcriptColumn.message"
description="Message with the number of transcripts available"
defaultMessage="{message}"
values={{ message: transcriptMessage }}
/>
</div>
);
};
TranscriptColumn.propTypes = {
row: {
original: {
transcript: PropTypes.arrayOf([PropTypes.string]).isRequired,
transcriptionStatus: PropTypes.string.isRequired,
}.isRequired,
}.isRequired,
};
export default injectIntl(TranscriptColumn);

View File

@@ -3,6 +3,7 @@ import ActiveColumn from './ActiveColumn';
import MoreInfoColumn from './MoreInfoColumn';
import StatusColumn from './StatusColumn';
import ThumbnailColumn from './ThumbnailColumn';
import TranscriptColumn from './TranscriptColumn';
export {
AccessColumn,
@@ -10,4 +11,5 @@ export {
MoreInfoColumn,
StatusColumn,
ThumbnailColumn,
TranscriptColumn,
};

View File

@@ -36,6 +36,7 @@ import {
FileTable,
StatusColumn,
ThumbnailColumn,
TranscriptColumn,
} from '../generic';
import TranscriptSettings from './transcript-settings';
import VideoThumbnail from './VideoThumbnail';
@@ -107,17 +108,15 @@ const VideosPage = ({
fileType: 'video',
};
const thumbnailPreview = (props) => VideoThumbnail({ ...props, handleAddThumbnail, videoImageSettings });
const infoModalSidebar = (video) => VideoInfoModalSidebar({ video });
const infoModalSidebar = (video, activeTab, setActiveTab) => (
VideoInfoModalSidebar({ video, activeTab, setActiveTab })
);
const maxFileSize = videoUploadMaxFileSize * 1073741824;
const transcriptColumn = {
id: 'transcriptStatus',
Header: 'Transcript',
accessor: 'transcriptStatus',
Cell: ({ row }) => {
const { transcripts } = row.original;
const numOfTranscripts = transcripts?.length;
return numOfTranscripts > 0 ? `(${numOfTranscripts}) available` : null;
},
Cell: ({ row }) => TranscriptColumn({ row }),
Filter: CheckboxFilter,
filter: 'exactTextCase',
filterChoices: [

View File

@@ -430,73 +430,94 @@ describe('FilesAndUploads', () => {
});
describe('card menu actions', () => {
it('should open video info', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
describe('Info', () => {
it('should open video info', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`)
.reply(201, { usageLocations: ['subsection - unit / block'] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`)
.reply(201, { usageLocations: ['subsection - unit / block'] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
});
expect(screen.getByText(messages.infoTitle.defaultMessage)).toBeVisible();
const { usageStatus } = store.getState().videos;
expect(usageStatus).toEqual(RequestStatus.SUCCESSFUL);
expect(screen.getByText('subsection - unit / block')).toBeVisible();
});
expect(screen.getByText(messages.infoTitle.defaultMessage)).toBeVisible();
it('should open video info modal and show info tab', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
const { usageStatus } = store.getState().videos;
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
});
expect(usageStatus).toEqual(RequestStatus.SUCCESSFUL);
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
expect(screen.getByText('subsection - unit / block')).toBeVisible();
});
const infoTab = screen.getAllByRole('tab')[0];
expect(infoTab).toBeVisible();
it('should open video info modal and show info tab', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
expect(infoTab).toHaveClass('active');
});
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
it('should open video info modal and show transcript tab', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
const infoTab = screen.getAllByRole('tab')[0];
expect(infoTab).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
});
expect(infoTab).toHaveClass('active');
});
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
it('should open video info modal and show transcript tab', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
expect(videoMenuButton).toBeVisible();
const transcriptTab = screen.getAllByRole('tab')[1];
await act(async () => {
fireEvent.click(transcriptTab);
});
expect(transcriptTab).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
expect(transcriptTab).toHaveClass('active');
});
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
it('should show transcript error', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
const transcriptTab = screen.getAllByRole('tab')[1];
await act(async () => {
fireEvent.click(transcriptTab);
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID3/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByText('Info'));
});
const transcriptTab = screen.getAllByRole('tab')[1];
await act(async () => {
fireEvent.click(transcriptTab);
});
expect(screen.getByText('Transcript (1)')).toBeVisible();
});
expect(transcriptTab).toBeVisible();
expect(transcriptTab).toHaveClass('active');
});
it('download button should download file', async () => {

View File

@@ -6,3 +6,6 @@ export const MIN_WIDTH = 640;
export const MIN_HEIGHT = 360;
export const ASPECT_RATIO = 16 / 9;
export const ASPECT_RATIO_ERROR_MARGIN = 0.1;
export const TRANSCRIPT_FAILURE_STATUSES = ['Transcript Failed', 'Partial Failure'];
export const VIDEO_PROCESSING_STATUSES = ['Uploading', 'In Progress', 'Uploaded'];
export const VIDEO_SUCCESS_STATUSES = ['Ready', 'Imported'];

View File

@@ -7,6 +7,8 @@ import {
MAX_WIDTH,
MIN_HEIGHT,
MIN_WIDTH,
VIDEO_PROCESSING_STATUSES,
VIDEO_SUCCESS_STATUSES,
} from './constants';
ensureConfig([
@@ -35,9 +37,9 @@ export const updateFileValues = (files) => {
const activeStatus = usageLocations?.length > 0 ? 'active' : 'inactive';
let uploadStatus = status;
if (status === 'Ready' || status === 'Imported') {
if (VIDEO_SUCCESS_STATUSES.includes(status)) {
uploadStatus = 'Success';
} else if (status === 'In Progress' || status === 'Uploaded') {
} else if (VIDEO_PROCESSING_STATUSES.includes(status)) {
uploadStatus = 'Processing';
}

View File

@@ -120,6 +120,7 @@ export const generateFetchVideosApiResponse = () => ({
status: 'Imported',
duration: 12333,
downloadLink: 'http://mOckID1.mp4',
fileSize: 213456354,
},
{
edx_video_id: 'mOckID5',
@@ -140,6 +141,8 @@ export const generateFetchVideosApiResponse = () => ({
status: 'Ready',
duration: null,
downloadLink: '',
transcription_status: 'Transcript Failed',
error_description: 'Unable to process transcript request',
},
],
concurrent_upload_limit: 4,

View File

@@ -8,13 +8,20 @@ import {
import InfoTab from './InfoTab';
import TranscriptTab from './TranscriptTab';
import messages from './messages';
import { TRANSCRIPT_FAILURE_STATUSES } from '../data/constants';
const VideoInfoModalSidebar = ({
video,
activeTab,
setActiveTab,
// injected
intl,
}) => (
<Tabs>
<Tabs
id="controlled-info-sidebar-tab"
activeKey={activeTab}
onSelect={(tab) => setActiveTab(tab)}
>
<Tab eventKey="fileInfo" title={intl.formatMessage(messages.infoTabTitle)}>
<InfoTab {...{ video }} />
</Tab>
@@ -24,6 +31,11 @@ const VideoInfoModalSidebar = ({
messages.transcriptTabTitle,
{ transcriptCount: video.transcripts.length },
)}
notification={TRANSCRIPT_FAILURE_STATUSES.includes(video.transcriptionStatus) && (
<span>
<span className="sr-only">{intl.formatMessage(messages.notificationScreenReaderText)}</span>
</span>
)}
>
<TranscriptTab {...{ video }} />
</Tab>
@@ -38,7 +50,10 @@ VideoInfoModalSidebar.propTypes = {
dateAdded: PropTypes.string.isRequired,
fileSize: PropTypes.number.isRequired,
transcripts: PropTypes.arrayOf(PropTypes.string),
transcriptionStatus: PropTypes.string.isRequired,
}),
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};

View File

@@ -11,6 +11,11 @@ const messages = defineMessages({
defaultMessage: 'Transcript ({transcriptCount})',
description: 'Title for info tab',
},
notificationScreenReaderText: {
id: 'course-authoring.video-uploads.file-info.transcriptTab.notification.screenReader.text',
defaultMessage: 'Transcription error',
description: 'Scrren reader text for transcript tab notification',
},
dateAddedTitle: {
id: 'course-authoring.video-uploads.file-info.infoTab.dateAdded.title',
defaultMessage: 'Date added',