Skip to content
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
12 changes: 10 additions & 2 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
"categories": {
"correctness": "warn"
},
"plugins": ["react"],
"rules": {
"eslint/no-unused-vars": "off",
"typescript/unbound-method": "off", // 🛑 TEMPORARY
"eslint/no-unused-vars": ["warn", {
// Allow using {ignoredProp, ...keepTheRest} to omit a prop like 'ignoredProp' from an object.
"ignoreRestSiblings": true,
}],
// We disable exhaustive-deps because: it's noisy, and we often include extra deps when we want a memoized thing to
// re-calculate after some change, even if we're not using that thing in the calculation.
"react-hooks/exhaustive-deps": "off",
// Rule of hooks is useful, but not on by default:
"react/rules-of-hooks": "warn",
"typescript/no-floating-promises": ["error", {
"allowForKnownSafeCalls": [
// queryClient.invalidateQueries returns a promise that can be awaited
Expand Down
2 changes: 1 addition & 1 deletion plugins/course-apps/teams/GroupEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const GroupEditor = ({
? (
<div className="d-flex flex-column card rounded mb-3 px-4 py-2 p-4" key="isDeleting">
<h4 className="mb-3">{intl.formatMessage(messages.groupDeleteHeading)}</h4>
{intl.formatMessage(messages.groupDeleteBody).split('\n').map(text => <p>{text}</p>)}
{intl.formatMessage(messages.groupDeleteBody).split('\n').map(text => <p key={text}>{text}</p>)}
<div className="d-flex flex-row justify-content-end">
<Button variant="muted" size="sm" onClick={cancelDeletion}>
{intl.formatMessage(messages.cancel)}
Expand Down
2 changes: 1 addition & 1 deletion src/advanced-settings/AdvancedSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ const AdvancedSettings = () => {
role="dialog"
actions={[
!isQueryPending ? (
<Button variant="tertiary" onClick={handleResetSettingsValues}>
<Button key="cancelBtn" variant="tertiary" onClick={handleResetSettingsValues}>
{intl.formatMessage(messages.buttonCancelText)}
</Button>
) : /* istanbul ignore next */ null,
Expand Down
4 changes: 1 addition & 3 deletions src/course-libraries/OutOfSyncAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
variant="info"
onClose={dismissAlert}
actions={[
<Button
onClick={onReview}
>
<Button key="review-btn" onClick={onReview}>
{intl.formatMessage(messages.outOfSyncCountAlertReviewBtn)}
</Button>,
]}
Expand Down
3 changes: 0 additions & 3 deletions src/course-outline/CourseOutline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { UnlinkModal } from '@src/generic/unlink-modal';
import AlertMessage from '@src/generic/alert-message';
import getPageHeadTitle from '@src/generic/utils';
import CourseOutlineHeaderActionsSlot from '@src/plugin-slots/CourseOutlineHeaderActionsSlot';
import { XBlock } from '@src/data/types';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useCourseOutlineContext } from './CourseOutlineContext';
import LegacyLibContentBlockAlert from '@src/course-libraries/LegacyLibContentBlockAlert';
Expand Down Expand Up @@ -72,8 +71,6 @@ const CourseOutline = () => {
closeUnlinkModal,
} = useCourseAuthoringContext();
const {
handleAddBlock,
handleAddAndOpenUnit,
currentSelection,
sections,
restoreSectionList,
Expand Down
2 changes: 1 addition & 1 deletion src/course-outline/CourseOutlineContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useToggle } from '@openedx/paragon';
import { arrayMove } from '@dnd-kit/sortable';

import { SelectionState, type UnitXBlock, type XBlock } from '@src/data/types';
import { SelectionState, type XBlock } from '@src/data/types';
import { useToggleWithValue } from '@src/hooks';
import { getBlockType } from '@src/generic/key-utils';
import { COURSE_BLOCK_NAMES } from '@src/constants';
Expand Down
2 changes: 0 additions & 2 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
enableCourseHighlightsEmailsQuery,
fetchCourseBestPracticesQuery,
fetchCourseLaunchQuery,
fetchCourseOutlineIndexQuery,
fetchCourseReindexQuery,
setVideoSharingOptionQuery,
dismissNotificationQuery,
Expand All @@ -52,7 +51,6 @@ const useCourseOutline = ({ courseId }) => {
handleAddBlock,
setCurrentSelection,
currentSelection,
isDuplicatingItem,
isDeleteModalOpen,
openDeleteModal,
closeDeleteModal,
Expand Down
3 changes: 3 additions & 0 deletions src/course-outline/page-alerts/PageAlerts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const PageAlerts = ({
onClose={onDismiss}
actions={[
<Button
key="learnMore"
href={discussionsIncontextLearnmoreUrl}
target="_blank"
>
Expand Down Expand Up @@ -267,6 +268,7 @@ const PageAlerts = ({
onClose={onDismiss}
actions={[
<Button
key="view-files"
as={Link}
to={getAssetsUrl()}
>
Expand Down Expand Up @@ -329,6 +331,7 @@ const PageAlerts = ({
onClose={onDismiss}
actions={[
<Button
key="view-files"
as={Link}
to={getAssetsUrl()}
>
Expand Down
8 changes: 3 additions & 5 deletions src/course-unit/CourseUnit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,12 @@ const CourseUnit = () => {
</TransitionReplace>
{courseUnit.upstreamInfo?.upstreamLink && (
<AlertMessage
title={intl.formatMessage(
description={intl.formatMessage(
messages.alertLibraryUnitReadOnlyText,
{
link: (
<Alert.Link
href={courseUnit.upstreamInfo.upstreamLink}
>
{intl.formatMessage(messages.alertLibraryUnitReadOnlyLinkText)}
<Alert.Link href={courseUnit.upstreamInfo.upstreamLink}>
<FormattedMessage {...messages.alertLibraryUnitReadOnlyLinkText} />
</Alert.Link>
),
},
Expand Down
2 changes: 2 additions & 0 deletions src/course-unit/clipboard/paste-notification/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
icon={WarningIcon}
dismissible
actions={[
// oxlint-disable-next-line react/jsx-key (Paragon <Alert> adds its own key for action buttons)
<ActionButton
courseId={courseId}
title={intl.formatMessage(messages.hasConflictingErrorsButtonText)}
Expand Down Expand Up @@ -87,6 +88,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
icon={InfoIcon}
dismissible
actions={[
// oxlint-disable-next-line react/jsx-key (Paragon <Alert> adds its own key for action buttons)
<ActionButton
courseId={courseId}
title={intl.formatMessage(messages.hasNewFilesButtonText)}
Expand Down
2 changes: 2 additions & 0 deletions src/course-unit/unit-sidebar/AddSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const AddNewContent = () => {
<Stack gap={2}>
{templatesByType.advanced?.templates.map((advancedTypeObj) => (
<BlockCardButton
key={advancedTypeObj.category}
blockType={advancedTypeObj.category}
name={advancedTypeObj.displayName}
onClick={() => handleSelection('advanced', advancedTypeObj.category)}
Expand All @@ -223,6 +224,7 @@ const AddNewContent = () => {
{blockTypes.map((blockTypeObj) => (
<BlockCardButton
{...blockTypeObj}
key={blockTypeObj.blockType}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was resulting in a React console warning, and eslint was not picking it up but oxlint now catches it.

installHook.js:1 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `AddNewContent`. See https://reactjs.org/link/warning-keys for more information.
    at BlockCardButton (webpack-internal:///./src/generic/sidebar/BlockCardButton.tsx:26:3)
    at AddNewContent (webpack-internal:///./src/course-unit/unit-sidebar/AddSidebar.tsx:90:88)

templates={templatesByType[blockTypeObj.blockType].templates}
onClick={() => handleSelection(blockTypeObj.blockType)}
onClickTemplate={(boilerplateName: string) => handleSelection(blockTypeObj.blockType, boilerplateName)}
Expand Down
2 changes: 2 additions & 0 deletions src/course-updates/CourseUpdates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const CourseUpdates = () => {
{courseUpdates.map((courseUpdate, index) => (
isInnerFormOpen(courseUpdate.id) ? (
<UpdateForm
key={courseUpdate.id}
close={closeUpdateForm}
requestType={requestType}
isInnerForm
Expand All @@ -175,6 +176,7 @@ const CourseUpdates = () => {
/>
) : (
<CourseUpdate
key={courseUpdate.id}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was resulting in a React console warning, and eslint was not picking it up but oxlint now catches it.

Warning: Each child in a list should have a unique "key" prop.
Check the render method of `CourseUpdates`. See https://reactjs.org/link/warning-keys for more information.
    at CourseUpdate (webpack-internal:///./src/course-updates/course-update/CourseUpdate.jsx:32:3)
    at CourseUpdates (webpack-internal:///./src/course-updates/CourseUpdates.tsx:69:87)

dateForUpdate={courseUpdate.date}
contentForUpdate={courseUpdate.content}
onEdit={() => handleOpenUpdateForm(REQUEST_TYPES.edit_update, courseUpdate)}
Expand Down
1 change: 1 addition & 0 deletions src/editors/EditorContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const EditorContainer: React.FC<Props> = ({
description={intl.formatMessage(messages.libraryBlockEditWarningDescription)}
actions={[
<Button
key="edit-warning"
destination={getLibraryBlockUrl()}
target="_blank"
rel="noopener noreferrer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const GroupFeedbackRow = ({
{answers.map((letter) => (
<Form.Checkbox
className="mr-4 mt-1"
key={letter.id}
value={letter.id}
checked={value.answers.indexOf(letter.id)}
isValid={value.answers.indexOf(letter.id) >= 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ const LicenseSelector = ({
onChange={(e) => onLicenseChange(e.target.value)}
>
{Object.entries(LicenseNames).map(([key, text]) => {
if (license === key) { return (<option value={LicenseTypes[key]} selected>{text}</option>); }
if (key === LicenseTypes.select) { return (<option hidden>{text}</option>); }
return (<option value={LicenseTypes[key]}>{text}</option>);
if (license === key) { return (<option key={key} value={LicenseTypes[key]} selected>{text}</option>); }
if (key === LicenseTypes.select) { return (<option key={key} hidden>{text}</option>); }
return (<option key={key} value={LicenseTypes[key]}>{text}</option>);
})}
</Form.Control>
{level !== LicenseLevel.course ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@openedx/paragon';

import { Check } from '@openedx/paragon/icons';
import { connect, useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { thunkActions, selectors } from '../../../../../../data/redux';
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
Expand Down Expand Up @@ -47,9 +47,8 @@ export const hooks = {
const LanguageSelector = ({
index, // For a unique id for the form control
language,
// Redux
openLanguages, // Only allow those languages not already associated with a transcript to be selected
}) => {
const openLanguages = useSelector(selectors.video.openLanguages);
const intl = useIntl();
const [localLang, setLocalLang] = React.useState(language);
const input = fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch(), localLang }) });
Expand Down Expand Up @@ -95,12 +94,14 @@ const LanguageSelector = ({
<Dropdown.Menu>
{Object.entries(videoTranscriptLanguages).map(([lang, text]) => {
if (language === lang) {
return (<Dropdown.Item>{text}<Icon className="text-primary-500" src={Check} /></Dropdown.Item>);
return <Dropdown.Item key={lang}>{text}<Icon className="text-primary-500" src={Check} /></Dropdown.Item>;
}
if (openLanguages.some(row => row.includes(lang))) {
return (<Dropdown.Item onClick={() => onLanguageChange({ newLang: lang })}>{text}</Dropdown.Item>);
return (
<Dropdown.Item key={lang} onClick={() => onLanguageChange({ newLang: lang })}>{text}</Dropdown.Item>
);
}
return (<Dropdown.Item className="disabled">{text}</Dropdown.Item>);
return (<Dropdown.Item key={lang} className="disabled">{text}</Dropdown.Item>);
})}
</Dropdown.Menu>
</Dropdown>
Expand All @@ -109,21 +110,9 @@ const LanguageSelector = ({
);
};

LanguageSelector.defaultProps = {
openLanguages: [],
};

LanguageSelector.propTypes = {
openLanguages: PropTypes.arrayOf(PropTypes.string),
index: PropTypes.number.isRequired,
language: PropTypes.string.isRequired,
};

export const mapStateToProps = (state) => ({
openLanguages: selectors.video.openLanguages(state),
});

export const mapDispatchToProps = {};

export const LanguageSelectorInternal = LanguageSelector; // For testing only
export default connect(mapStateToProps, mapDispatchToProps)(LanguageSelector);
export default LanguageSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React from 'react';
import {
render, screen, initializeMocks, fireEvent,
} from '@src/testUtils';
import { useDispatch } from 'react-redux';
import LanguageSelector from './LanguageSelector';
import { selectors } from '../../../../../../data/redux';
import { thunkActions, selectors } from '../../../../../../data/redux';

const lang1 = 'kLinGon';
const lang1Code = 'kl';
Expand All @@ -12,6 +13,14 @@ const lang2Code = 'el';
const lang3 = 'sImLisH';
const lang3Code = 'sl';

jest.mock('react-redux', () => {
const dispatchFn = jest.fn().mockName('mockDispatch');
return {
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => dispatchFn),
};
});

jest.mock('../../../../../../data/constants/video', () => ({
videoTranscriptLanguages: {
[lang1Code]: lang1,
Expand All @@ -20,6 +29,20 @@ jest.mock('../../../../../../data/constants/video', () => ({
},
}));

jest.mock('../../../../../../data/redux', () => ({
thunkActions: {
video: {
updateTranscriptLanguage: jest.fn((args) => ({ updateTranscriptLanguage: args })).mockName('thunkActions.video.updateTranscriptLanguage'),
uploadTranscript: jest.fn().mockName('thunkActions.video.uploadTranscript'),
},
},
selectors: {
video: {
openLanguages: jest.fn(),
},
},
}));

describe('LanguageSelector', () => {
const props = {
onSelect: jest.fn().mockName('props.OnSelect'),
Expand Down Expand Up @@ -55,4 +78,19 @@ describe('LanguageSelector', () => {
const disabledItems = container.querySelectorAll('.disabled.dropdown-item');
expect(disabledItems.length).toBe(3);
});

test('clicking an open language item dispatches updateTranscriptLanguage', () => {
const mockDispatch = jest.fn();
useDispatch.mockReturnValue(mockDispatch);
const { video } = selectors;
jest.spyOn(video, 'openLanguages').mockReturnValue([[lang2Code, lang2], [lang3Code, lang3]]);
render(<LanguageSelector index={1} language={lang1Code} />);
fireEvent.click(screen.getByRole('button', { name: 'Languages' }));
fireEvent.click(screen.getByText(lang2));
expect(thunkActions.video.updateTranscriptLanguage).toHaveBeenCalledWith({
newLanguageCode: lang2Code,
languageBeforeChange: lang1Code,
});
expect(mockDispatch).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const TranscriptWidget = ({
<Form.Group className="border-primary-100 border-bottom">
{transcripts.map((language, index) => (
<Transcript
key={language}
language={language}
transcriptUrl={selectedVideoTranscriptUrls[language]}
index={index}
Expand Down
2 changes: 1 addition & 1 deletion src/files-and-videos/files-page/FileValidationModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const FileValidationModal = ({
<ModalDialog.Body>
<FormattedMessage {...messages.overwriteConfirmMessage} />
<ul className="mt-2">
{Object.keys(duplicateFiles).map(file => <li>{file}</li>)}
{Object.keys(duplicateFiles).map(file => <li key={file}>{file}</li>)}
</ul>
</ModalDialog.Body>
<ModalDialog.Footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const SortAndFilterModal = ({
isInline
>
{filterOptions.map(({ name, value }) => (
<Form.Checkbox {...{ value, key: value }}>{name}</Form.Checkbox>
<Form.Checkbox key={value} value={value}>{name}</Form.Checkbox>
))}
</Form.CheckboxSet>
</Form.Group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ const TranscriptTab = ({
))}
</ul>
</ErrorAlert>
{previousSelection.map(transcript => (
{previousSelection.map((transcript, idx) => (
<Transcript
// eslint-disable-next-line react/no-array-index-key
key={idx}
{...{
languages,
transcript,
Expand Down
Loading