feat: add notification of transcription error (#715)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MoreInfoColumn,
|
||||
StatusColumn,
|
||||
ThumbnailColumn,
|
||||
TranscriptColumn,
|
||||
} from './table-custom-columns';
|
||||
|
||||
export {
|
||||
@@ -22,4 +23,5 @@ export {
|
||||
MoreInfoColumn,
|
||||
StatusColumn,
|
||||
ThumbnailColumn,
|
||||
TranscriptColumn,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user