fix: transcript tab layout (#686)
* fix: transcript tab layout * fix: console warnings for missing config values
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ModalDialog,
|
||||
Stack,
|
||||
Truncate,
|
||||
} from '@edx/paragon';
|
||||
|
||||
@@ -30,6 +31,7 @@ const InfoModal = ({
|
||||
size="lg"
|
||||
hasCloseButton
|
||||
data-testid="file-info-modal"
|
||||
style={{ minHeight: '799px' }}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
@@ -44,25 +46,29 @@ const InfoModal = ({
|
||||
<hr />
|
||||
<div className="row flex-nowrap m-0 mt-4">
|
||||
<div className="col-7 mr-3">
|
||||
<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' }}
|
||||
/>
|
||||
<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)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row m-0 pt-3 font-weight-bold">
|
||||
<FormattedMessage {...messages.usageTitle} />
|
||||
</div>
|
||||
<UsageMetricsMessages {...{ usageLocations: file?.usageLocations, usagePathStatus, error }} />
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,26 @@ export const getLanguages = (availableLanguages) => {
|
||||
return languages;
|
||||
};
|
||||
|
||||
export const getSortedTranscripts = (languages, transcripts) => {
|
||||
const transcriptDisplayNames = [];
|
||||
transcripts.forEach(transcript => {
|
||||
const displayName = languages[transcript];
|
||||
transcriptDisplayNames.push(displayName);
|
||||
});
|
||||
|
||||
const sortedTranscripts = transcriptDisplayNames.sort();
|
||||
const sortedTranscriptCodes = [];
|
||||
sortedTranscripts.forEach(transcript => {
|
||||
Object.entries(languages).forEach(([key, value]) => {
|
||||
if (value === transcript) {
|
||||
sortedTranscriptCodes.push(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return sortedTranscriptCodes;
|
||||
};
|
||||
|
||||
export const getSupportedFormats = (supportedFileFormats) => {
|
||||
if (isEmpty(supportedFileFormats)) {
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isEmpty } from 'lodash';
|
||||
@@ -6,7 +6,7 @@ import { ErrorAlert } from '@edx/frontend-lib-content-components';
|
||||
import { Button, Stack } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getLanguages } from '../data/utils';
|
||||
import { getLanguages, getSortedTranscripts } from '../data/utils';
|
||||
import Transcript from './transcript-item';
|
||||
import {
|
||||
deleteVideoTranscript,
|
||||
@@ -23,6 +23,7 @@ const TranscriptTab = ({
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const divRef = useRef(null);
|
||||
const { transcriptStatus, errors } = useSelector(state => state.videos);
|
||||
const {
|
||||
transcriptAvailableLanguages,
|
||||
@@ -35,13 +36,22 @@ const TranscriptTab = ({
|
||||
} = videoTranscriptSettings;
|
||||
const { transcripts, id, displayName } = video;
|
||||
const languages = getLanguages(transcriptAvailableLanguages);
|
||||
let sortedTranscripts = getSortedTranscripts(languages, transcripts);
|
||||
const [previousSelection, setPreviousSelection] = useState(sortedTranscripts);
|
||||
|
||||
const [previousSelection, setPreviousSelection] = useState(transcripts);
|
||||
useEffect(() => {
|
||||
dispatch(resetErrors({ errorType: 'transcript' }));
|
||||
setPreviousSelection(transcripts);
|
||||
sortedTranscripts = getSortedTranscripts(languages, transcripts);
|
||||
setPreviousSelection(sortedTranscripts);
|
||||
}, [transcripts]);
|
||||
|
||||
const handleAddEmptyTranscript = () => {
|
||||
setPreviousSelection(['', ...previousSelection]);
|
||||
if (divRef?.current?.scrollTo) {
|
||||
divRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTranscript = (data, actionType) => {
|
||||
const {
|
||||
language,
|
||||
@@ -52,7 +62,8 @@ const TranscriptTab = ({
|
||||
switch (actionType) {
|
||||
case 'delete':
|
||||
if (isEmpty(language)) {
|
||||
const updatedSelection = previousSelection.filter(selection => selection !== '');
|
||||
const updatedSelection = previousSelection;
|
||||
updatedSelection.shift();
|
||||
setPreviousSelection(updatedSelection);
|
||||
} else {
|
||||
dispatch(deleteVideoTranscript({
|
||||
@@ -87,38 +98,42 @@ const TranscriptTab = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={3} className="mt-3">
|
||||
<ErrorAlert
|
||||
hideHeading={false}
|
||||
isError={transcriptStatus === RequestStatus.FAILED && !isEmpty(errors.transcript)}
|
||||
>
|
||||
<ul className="p-0">
|
||||
{errors.transcript.map(message => (
|
||||
<li key={`transcript-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ErrorAlert>
|
||||
{previousSelection.map(transcript => (
|
||||
<Transcript
|
||||
{...{
|
||||
languages,
|
||||
transcript,
|
||||
previousSelection,
|
||||
handleTranscript,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="link"
|
||||
iconBefore={Add}
|
||||
size="sm"
|
||||
className="text-primary-500 justify-content-start pl-0"
|
||||
onClick={() => setPreviousSelection([...previousSelection, ''])}
|
||||
>
|
||||
{intl.formatMessage(messages.uploadButtonLabel)}
|
||||
</Button>
|
||||
<Stack gap={3}>
|
||||
<div ref={divRef} style={{ overflowY: 'scroll', height: '310px' }} className="px-1 py-2">
|
||||
<ErrorAlert
|
||||
hideHeading={false}
|
||||
isError={transcriptStatus === RequestStatus.FAILED && !isEmpty(errors.transcript)}
|
||||
>
|
||||
<ul className="p-0">
|
||||
{errors.transcript.map(message => (
|
||||
<li key={`transcript-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ErrorAlert>
|
||||
{previousSelection.map(transcript => (
|
||||
<Transcript
|
||||
{...{
|
||||
languages,
|
||||
transcript,
|
||||
previousSelection,
|
||||
handleTranscript,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-top border-light-400">
|
||||
<Button
|
||||
variant="link"
|
||||
iconBefore={Add}
|
||||
size="sm"
|
||||
className="text-primary-500 justify-content-start pl-0 pt-3"
|
||||
onClick={handleAddEmptyTranscript}
|
||||
>
|
||||
{intl.formatMessage(messages.uploadButtonLabel)}
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -281,13 +281,11 @@ describe('TranscriptTab', () => {
|
||||
});
|
||||
|
||||
const englishOption = screen.getByText('English');
|
||||
const arabicOption = screen.getAllByRole('button', { name: 'Arabic' })[0];
|
||||
await act(async () => {
|
||||
expect(arabicOption).toHaveClass('disabled');
|
||||
fireEvent.click(englishOption);
|
||||
});
|
||||
|
||||
const menuButton = screen.getByTestId('fr-transcript-menu');
|
||||
const menuButton = screen.getByTestId('ar-transcript-menu');
|
||||
await waitFor(() => {
|
||||
fireEvent.click(menuButton);
|
||||
});
|
||||
@@ -302,8 +300,6 @@ describe('TranscriptTab', () => {
|
||||
|
||||
await act(async () => {
|
||||
const addFileInput = screen.getAllByLabelText('file-input')[0];
|
||||
expect(addFileInput).toBeInTheDocument();
|
||||
|
||||
userEvent.upload(addFileInput, file);
|
||||
});
|
||||
const addStatus = store.getState().videos.transcriptStatus;
|
||||
@@ -312,7 +308,7 @@ describe('TranscriptTab', () => {
|
||||
|
||||
const updatedTranscripts = store.getState().models.videos[defaultProps.id].transcripts;
|
||||
|
||||
expect(updatedTranscripts).toEqual(['ar', 'en']);
|
||||
expect(updatedTranscripts).toEqual(['fr', 'en']);
|
||||
});
|
||||
|
||||
it('should show error message', async () => {
|
||||
@@ -320,8 +316,6 @@ describe('TranscriptTab', () => {
|
||||
|
||||
await act(async () => {
|
||||
const addFileInput = screen.getAllByLabelText('file-input')[0];
|
||||
expect(addFileInput).toBeInTheDocument();
|
||||
|
||||
userEvent.upload(addFileInput, file);
|
||||
});
|
||||
|
||||
@@ -329,7 +323,7 @@ describe('TranscriptTab', () => {
|
||||
|
||||
expect(addStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getAllByText('Failed to replace fr with en.')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('Failed to replace ar with en.')[0]).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, Icon } from '@edx/paragon';
|
||||
import { Check } from '@edx/paragon/icons';
|
||||
import { Check, ExpandMore } from '@edx/paragon/icons';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const LanguageSelect = ({
|
||||
@@ -15,7 +15,8 @@ const LanguageSelect = ({
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
variant="teritary"
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
className="border border-gray-700 justify-content-between"
|
||||
style={{ minWidth: '100%' }}
|
||||
id={`language-select-dropdown-${currentSelection}`}
|
||||
@@ -23,28 +24,33 @@ const LanguageSelect = ({
|
||||
>
|
||||
{currentSelection}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="m-0" style={{ height: '300px', overflowY: 'scroll' }}>
|
||||
{Object.entries(options).map(([valueKey, text]) => {
|
||||
if (valueKey === value) {
|
||||
<Dropdown.Menu className="m-0">
|
||||
<div style={{ height: '230px', overflowY: 'scroll' }}>
|
||||
{Object.entries(options).map(([valueKey, text]) => {
|
||||
if (valueKey === value) {
|
||||
return (
|
||||
<Dropdown.Item className="small" key={`${valueKey}-item`}>
|
||||
<Icon size="inline" src={Check} className="m-n2" /><span className="pl-3">{text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
if (!previousSelection.includes(valueKey)) {
|
||||
return (
|
||||
<Dropdown.Item className="small" onClick={() => handleSelect(valueKey)} key={`${valueKey}-item`}>
|
||||
<span className="pl-3">{text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Dropdown.Item key={`${valueKey}-item`}>
|
||||
<Icon size="inline" src={Check} className="m-n2" /><span className="pl-3">{text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
if (!previousSelection.includes(valueKey)) {
|
||||
return (
|
||||
<Dropdown.Item onClick={() => handleSelect(valueKey)} key={`${valueKey}-item`}>
|
||||
<Dropdown.Item disabled className="small" key={`${valueKey}-item`}>
|
||||
<span className="pl-3">{text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Dropdown.Item disabled key={`${valueKey}-item`}>
|
||||
<span className="pl-3">{text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
<div className="row justify-content-center">
|
||||
<Icon src={ExpandMore} size="xs" />
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Card,
|
||||
@@ -27,6 +27,10 @@ const Transcript = ({
|
||||
const [newLanguage, setNewLanguage] = useState(transcript);
|
||||
const language = transcript;
|
||||
|
||||
useEffect(() => {
|
||||
setNewLanguage(transcript);
|
||||
}, [transcript]);
|
||||
|
||||
const input = useFileInput({
|
||||
onAddFile: (file) => {
|
||||
handleTranscript({
|
||||
@@ -49,7 +53,7 @@ const Transcript = ({
|
||||
return (
|
||||
<>
|
||||
{isConfirmationOpen ? (
|
||||
<Card className="mb-2">
|
||||
<Card className="my-2">
|
||||
<Card.Header title={(<FormattedMessage {...messages.deleteConfirmationHeader} />)} />
|
||||
<Card.Body>
|
||||
<Card.Section>
|
||||
|
||||
@@ -89,6 +89,10 @@ initialize({
|
||||
BBB_LEARN_MORE_URL: process.env.BBB_LEARN_MORE_URL || '',
|
||||
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
|
||||
STUDIO_SHORT_NAME: process.env.STUDIO_SHORT_NAME || null,
|
||||
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL || null,
|
||||
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL || null,
|
||||
SHOW_ACCESSIBILITY_PAGE: process.env.SHOW_ACCESSIBILITY_PAGE || false,
|
||||
NOTIFICATION_FEEDBACK_URL: process.env.NOTIFICATION_FEEDBACK_URL || null,
|
||||
}, 'CourseAuthoringConfig');
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user