diff --git a/config/webpack.config-dev.js b/config/webpack.config-dev.js index 76461728e..4a0dd6661 100644 --- a/config/webpack.config-dev.js +++ b/config/webpack.config-dev.js @@ -28,8 +28,8 @@ export default { // switch the source map generation when debugging // note, we used 'eval-source-map' before, but since webpack 5.100, it breaks the build // (causes 'SyntaxError: redeclaration of function normalize') - devtool: 'inline-source-map', - //devtool: false, // turn it off completely + // devtool: 'inline-source-map', + devtool: false, // turn it off completely entry: path.join(__dirname, '..', 'src/client.js'), output: { diff --git a/src/components/Solutions/SolutionFiles/SolutionFiles.js b/src/components/Solutions/SolutionFiles/SolutionFiles.js index 13542e7eb..01cba57e4 100644 --- a/src/components/Solutions/SolutionFiles/SolutionFiles.js +++ b/src/components/Solutions/SolutionFiles/SolutionFiles.js @@ -145,7 +145,7 @@ const SolutionFiles = ({ + content.replaceAll('\r', '')); @@ -45,10 +45,15 @@ const SourceCodeBox = ({ reviewClosed = false, collapsable = false, isOpen = true, + highlightOverrides = EMPTY_OBJ, + setHighlightOverride = null, }) => { const res = fileContentsSelector(parentId, entryName); const [clipboardCopied, setClipboardCopied] = useState(false); const [onlyComments, setOnlyComments] = useState(false); + + const fileExtension = getFileExtensionLC(name); + return ( )} - {name} + {name} {download && ( )} + {setHighlightOverride && ( + + )} + {diffMode && ( <> @@ -291,6 +307,7 @@ const SourceCodeBox = ({ removeComment={removeComment} reviewClosed={reviewClosed} onlyComments={reviewClosed && onlyComments} + highlightOverrides={highlightOverrides} /> )} @@ -319,6 +336,8 @@ SourceCodeBox.propTypes = { reviewClosed: PropTypes.bool, collapsable: PropTypes.bool, isOpen: PropTypes.bool, + highlightOverrides: PropTypes.object, + setHighlightOverride: PropTypes.func, }; export default SourceCodeBox; diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js new file mode 100644 index 000000000..015a128a9 --- /dev/null +++ b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js @@ -0,0 +1,149 @@ +import React, { useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Overlay, Popover, FormSelect, ButtonGroup } from 'react-bootstrap'; + +import Button from '../../widgets/TheButton'; +import Icon, { CloseIcon, RefreshIcon, SaveIcon } from '../../icons'; + +import { getPrismModeFromExtension, PRISM_SUPPORTED_LANGUAGES } from '../../helpers/syntaxHighlighting.js'; + +export const localStorageHighlightOverridesKey = 'SourceCodeViewer.highlightOverrides'; + +const SourceCodeHighlightingSelector = ({ + id, + fullButton = false, + extension, + initialMode = null, + onChange, + ...props +}) => { + const defaultMode = getPrismModeFromExtension(extension); + const target = useRef(null); + const [visible, setVisible] = useState(false); + const [selectedMode, setSelectedMode] = useState(initialMode !== null ? initialMode : defaultMode); + + useEffect(() => { + setSelectedMode(initialMode !== null ? initialMode : defaultMode); + }, [initialMode, extension]); + + const clickHandler = ev => { + ev.stopPropagation(); + setVisible(!visible); + }; + + const changeHandler = ev => { + setSelectedMode(ev.target.value); + }; + + return ( + <> + {fullButton ? ( + + ) : ( + + )} + + + {props => ( + ev.stopPropagation()} className="highlighting-selector" {...props}> + + {extension ? ( + <> + {' '} + *.{extension} + + ) : ( + + )} + + + + + {PRISM_SUPPORTED_LANGUAGES.map(lang => ( + + ))} + + + + + {selectedMode !== defaultMode && ( + + )} + + + + + )} + + + ); +}; + +SourceCodeHighlightingSelector.propTypes = { + id: PropTypes.string.isRequired, + fullButton: PropTypes.bool, + extension: PropTypes.string.isRequired, + initialMode: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +export default SourceCodeHighlightingSelector; diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css index 3103baf8e..a16d4af82 100644 --- a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css +++ b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css @@ -156,3 +156,7 @@ pre .sourceCodeViewerComments, code .sourceCodeViewerComments { .sourceCodeViewer.addComment .scvAddButton:hover::before { opacity: 1; } + +.popover.highlighting-selector { + --bs-popover-max-width: 400px; +} diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js index 1388f1c06..827b2f1c5 100644 --- a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js +++ b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js @@ -7,7 +7,7 @@ import 'prismjs/themes/prism.css'; import ReviewCommentForm, { newCommentFormInitialValues } from '../../forms/ReviewCommentForm'; import { getPrismModeFromExtension } from '../../helpers/syntaxHighlighting.js'; -import { getFileExtensionLC, canUseDOM } from '../../../helpers/common.js'; +import { getFileExtensionLC, canUseDOM, EMPTY_OBJ } from '../../../helpers/common.js'; import SourceCodeComment from './SourceCodeComment.js'; import './SourceCodeViewer.css'; @@ -196,10 +196,17 @@ class SourceCodeViewer extends React.Component { }); render() { - const { name, content = '', addComment } = this.props; + const { name, content = '', addComment, highlightOverrides = EMPTY_OBJ } = this.props; + + const extension = getFileExtensionLC(name); + const language = + highlightOverrides[extension] !== undefined + ? highlightOverrides[extension] + : getPrismModeFromExtension(extension); + return canUseDOM ? ( ext => { - ext = ext.trim().toLowerCase(); - if (ext === '') { - return mapping.makefile; // makefile has no extension - } else if (mapping[ext]) { - return mapping[ext]; // mapping found - } else { - return mapping.cpp; // C/C++ is default - } -}; +export const createExtensionTranslator = + (mapping, defaultDefault = null) => + (ext, defaultValue = defaultDefault) => { + ext = ext.trim().toLowerCase(); + if (ext === '') { + return mapping.makefile; // makefile has no extension + } else if (mapping[ext]) { + return mapping[ext]; // mapping found + } else { + return defaultValue; + } + }; const prismMapping = { ...commonMapping, @@ -61,5 +63,302 @@ const prismMapping = { xml: 'markup', }; -export const getAceModeFromExtension = createExtensionTranslator(aceMapping); -export const getPrismModeFromExtension = createExtensionTranslator(prismMapping); +export const getAceModeFromExtension = createExtensionTranslator(aceMapping, aceMapping.cpp); +export const getPrismModeFromExtension = createExtensionTranslator(prismMapping, null); + +export const PRISM_SUPPORTED_LANGUAGES = [ + 'abap', + 'abnf', + 'actionscript', + 'ada', + 'agda', + 'al', + 'antlr4', + 'apacheconf', + 'apex', + 'apl', + 'applescript', + 'aql', + 'arduino', + 'arff', + 'armasm', + 'arturo', + 'asciidoc', + 'asm6502', + 'asmatmel', + 'aspnet', + 'autohotkey', + 'autoit', + 'avisynth', + 'awk', + 'bash', + 'basic', + 'batch', + 'bbcode', + 'bbj', + 'bicep', + 'birb', + 'bison', + 'bnf', + 'bqn', + 'brainfuck', + 'brightscript', + 'bro', + 'bsl', + 'c', + 'cfscript', + 'chaiscript', + 'cil', + 'cilkc', + 'cilkcpp', + 'clike', + 'clojure', + 'cmake', + 'cobol', + 'coffeescript', + 'concurnas', + 'cooklang', + 'coq', + 'cpp', + 'crystal', + 'csharp', + 'cshtml', + 'csp', + 'cssExtras', + 'css', + 'csv', + 'cue', + 'cypher', + 'd', + 'dart', + 'dataweave', + 'dax', + 'dhall', + 'diff', + 'django', + 'dnsZoneFile', + 'docker', + 'dot', + 'ebnf', + 'editorconfig', + 'eiffel', + 'ejs', + 'elixir', + 'elm', + 'erb', + 'erlang', + 'etlua', + 'excelFormula', + 'factor', + 'falselang', + 'firestoreSecurityRules', + 'flow', + 'fortran', + 'fsharp', + 'ftl', + 'gap', + 'gcode', + 'gdscript', + 'gedcom', + 'gettext', + 'gherkin', + 'git', + 'glsl', + 'gml', + 'gn', + 'goModule', + 'go', + 'gradle', + 'graphql', + 'groovy', + 'haml', + 'handlebars', + 'haskell', + 'haxe', + 'hcl', + 'hlsl', + 'hoon', + 'hpkp', + 'hsts', + 'http', + 'ichigojam', + 'icon', + 'icuMessageFormat', + 'idris', + 'iecst', + 'ignore', + 'inform7', + 'ini', + 'io', + 'j', + 'java', + 'javadoc', + 'javadoclike', + 'javascript', + 'javastacktrace', + 'jexl', + 'jolie', + 'jq', + 'jsExtras', + 'jsTemplates', + 'jsdoc', + 'json', + 'json5', + 'jsonp', + 'jsstacktrace', + 'jsx', + 'julia', + 'keepalived', + 'keyman', + 'kotlin', + 'kumir', + 'kusto', + 'latex', + 'latte', + 'less', + 'lilypond', + 'linkerScript', + 'liquid', + 'lisp', + 'livescript', + 'llvm', + 'log', + 'lolcode', + 'lua', + 'magma', + 'makefile', + 'markdown', + 'markupTemplating', + 'markup', + 'mata', + 'matlab', + 'maxscript', + 'mel', + 'mermaid', + 'metafont', + 'mizar', + 'mongodb', + 'monkey', + 'moonscript', + 'n1ql', + 'n4js', + 'nand2tetrisHdl', + 'naniscript', + 'nasm', + 'neon', + 'nevod', + 'nginx', + 'nim', + 'nix', + 'nsis', + 'objectivec', + 'ocaml', + 'odin', + 'opencl', + 'openqasm', + 'oz', + 'parigp', + 'parser', + 'pascal', + 'pascaligo', + 'pcaxis', + 'peoplecode', + 'perl', + 'php', + 'phpdoc', + 'plantUml', + 'plsql', + 'powerquery', + 'powershell', + 'processing', + 'prolog', + 'promql', + 'properties', + 'protobuf', + 'psl', + 'pug', + 'puppet', + 'pure', + 'purebasic', + 'purescript', + 'python', + 'q', + 'qml', + 'qore', + 'qsharp', + 'r', + 'racket', + 'reason', + 'regex', + 'rego', + 'renpy', + 'rescript', + 'rest', + 'rip', + 'roboconf', + 'robotframework', + 'ruby', + 'rust', + 'sas', + 'sass', + 'scala', + 'scheme', + 'scss', + 'shellSession', + 'smali', + 'smalltalk', + 'smarty', + 'sml', + 'solidity', + 'soy', + 'sparql', + 'splunkSpl', + 'sqf', + 'sql', + 'squirrel', + 'stan', + 'stata', + 'stylus', + 'supercollider', + 'swift', + 'systemd', + 't4Cs', + 't4Templating', + 't4Vb', + 'tap', + 'tcl', + 'textile', + 'toml', + 'tremor', + 'tsx', + 'tt2', + 'turtle', + 'twig', + 'typescript', + 'typoscript', + 'unrealscript', + 'uorazor', + 'uri', + 'v', + 'vala', + 'vbnet', + 'velocity', + 'verilog', + 'vhdl', + 'vim', + 'visualBasic', + 'warpscript', + 'wasm', + 'webIdl', + 'wgsl', + 'wiki', + 'wolfram', + 'wren', + 'xeora', + 'xmlDoc', + 'xojo', + 'xquery', + 'yaml', + 'yang', + 'zig', +]; diff --git a/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js b/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js index 59390d34f..d865dd133 100644 --- a/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js +++ b/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js @@ -4,18 +4,25 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { Dropdown, DropdownButton, Modal } from 'react-bootstrap'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; import { lruMemoize } from 'reselect'; import Button from '../../components/widgets/TheButton'; import Callout from '../../components/widgets/Callout'; -import { DownloadIcon, LoadingIcon } from '../../components/icons'; +import { CopyIcon, CopySuccessIcon, DownloadIcon, LoadingIcon } from '../../components/icons'; import { download } from '../../redux/modules/files.js'; import { fetchContentIfNeeded } from '../../redux/modules/filesContent.js'; import { getFilesContent } from '../../redux/selectors/files.js'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; -import SourceCodeViewer from '../../components/helpers/SourceCodeViewer'; +import SourceCodeViewer, { + SourceCodeHighlightingSelector, + localStorageHighlightOverridesKey, +} from '../../components/helpers/SourceCodeViewer'; + import DownloadSolutionArchiveContainer from '../DownloadSolutionArchiveContainer'; import UsersNameContainer from '../UsersNameContainer'; +import { getFileExtensionLC } from '../../helpers/common.js'; +import { storageGetItem, storageSetItem } from '../../helpers/localStorage.js'; import * as styles from './sourceCode.less'; @@ -40,10 +47,15 @@ const preprocessFiles = lruMemoize(files => ); class SourceCodeViewerContainer extends Component { + state = { clipboardCopied: false, highlightOverrides: {} }; + componentDidMount() { const { fileId, loadAsync } = this.props; if (fileId !== null) { loadAsync(); + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } } @@ -53,9 +65,21 @@ class SourceCodeViewerContainer extends Component { (this.props.fileId !== prevProps.fileId || this.props.zipEntry !== prevProps.zipEntry) ) { this.props.loadAsync(); + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } } + setHighlightOverride = (extension, mode) => { + const { [extension]: _, ...newOverrides } = this.state.highlightOverrides; + if (mode || mode === '') { + newOverrides[extension] = mode; + } + this.setState({ highlightOverrides: newOverrides }); + storageSetItem(localStorageHighlightOverridesKey, newOverrides); + }; + render() { const { show, @@ -72,6 +96,8 @@ class SourceCodeViewerContainer extends Component { submittedBy = null, } = this.props; + const fileExtension = getFileExtensionLC(fileName); + return ( {content => ( - + {files => ( <> {preprocessFiles(files).map(f => ( @@ -113,11 +145,36 @@ class SourceCodeViewerContainer extends Component { ))} - + {!content.tooLarge && !content.malformedCharacters && ( + { + this.setState({ clipboardCopied: true }); + window.setTimeout(() => this.setState({ clipboardCopied: false }), 2000); + }}> + + + )} + {files.length > 0 && ( {content.content} ) : ( - + )}
diff --git a/src/locales/cs.json b/src/locales/cs.json index 3e9478566..ea9186c6f 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -1894,6 +1894,8 @@ "app.solutionSourceCodes.diffModal.title": "Porovnat dvě řešení a zobrazit rozdíly", "app.solutionSourceCodes.downloadTooltip": "Stáhnout soubor", "app.solutionSourceCodes.fullWidthTooltip": "Povolit celou šířku slouců, i když celková šíře boxu překročí šířku obrazovky.", + "app.solutionSourceCodes.highlightingTitle": "Zvýraznění syntaxe pro soubory s příponou", + "app.solutionSourceCodes.highlightingTitleNoExtension": "Zvýraznění syntaxe pro soubory bez přípony", "app.solutionSourceCodes.isBeingComparedWith": "... je porovnáváno s ...", "app.solutionSourceCodes.left": "Nalevo", "app.solutionSourceCodes.malformedTooltip": "Tento soubor neobsahuje běžný text v kódování UTF-8, takže není možne jej zobrazit jako zdrojový kód.", @@ -1902,6 +1904,7 @@ "app.solutionSourceCodes.mappingModal.resetButton": "Výchozí mapování", "app.solutionSourceCodes.mappingModal.title": "Upravit mapování porovnávaných souborů", "app.solutionSourceCodes.noDiffWithFile": "žádný odpovídající soubor pro porovnání nebyl nalezen", + "app.solutionSourceCodes.noHighlighting": "žádné zvýraznění", "app.solutionSourceCodes.restrictWidthTooltip": "Omezit šířky sloupců, aby každý zabíral polovinu obrazovky.", "app.solutionSourceCodes.reviewClosedAuthorInfoNoIssues": "Vaše řešení bylo revidováno a nejsou k němu vedeny žádné připomínky.", "app.solutionSourceCodes.reviewClosedAuthorInfoWithIssues": "Vaše řešení bylo revidováno. Máte celkem {issues} {issues, plural, one {připomínku} =2 {připomínky} =3 {připomínky} =4 {připomínky} other {připomínek}} k vyřešení.", @@ -2101,6 +2104,7 @@ "generic.createdAt": "Vytvořeno", "generic.creating": "Vytváření...", "generic.creationFailed": "Vytvoření se nezdařilo. Prosíme, opakujte akci později.", + "generic.default": "výchozí", "generic.delete": "Smazat", "generic.deleteFailed": "Smazání selhalo", "generic.deleted": "Smazáno", diff --git a/src/locales/en.json b/src/locales/en.json index e7041788a..18deee345 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1894,6 +1894,8 @@ "app.solutionSourceCodes.diffModal.title": "Compare two solutions and display differences", "app.solutionSourceCodes.downloadTooltip": "Download the file.", "app.solutionSourceCodes.fullWidthTooltip": "Enable full width of each column even if the box exceeds the screen width.", + "app.solutionSourceCodes.highlightingTitle": "Highlighting for files with extension", + "app.solutionSourceCodes.highlightingTitleNoExtension": "Highlighting for files without extension", "app.solutionSourceCodes.isBeingComparedWith": "... is being compared with ...", "app.solutionSourceCodes.left": "Left side", "app.solutionSourceCodes.malformedTooltip": "The file is not a valid UTF-8 text file so it cannot be properly displayed as a source code.", @@ -1902,6 +1904,7 @@ "app.solutionSourceCodes.mappingModal.resetButton": "Reset mapping", "app.solutionSourceCodes.mappingModal.title": "Adjust mapping of compared files", "app.solutionSourceCodes.noDiffWithFile": "no corresponding file for the comparison found", + "app.solutionSourceCodes.noHighlighting": "no highlighting", "app.solutionSourceCodes.restrictWidthTooltip": "Restrict the width of each column to the half of the screen.", "app.solutionSourceCodes.reviewClosedAuthorInfoNoIssues": "Your solution was reviewed and no issues were reported.", "app.solutionSourceCodes.reviewClosedAuthorInfoWithIssues": "Your solution was reviewed. You have {issues} {issues, plural, one {issue} other {issues}} to fix.", @@ -2101,6 +2104,7 @@ "generic.createdAt": "Created at", "generic.creating": "Creating...", "generic.creationFailed": "Creation failed. Please try again later.", + "generic.default": "default", "generic.delete": "Delete", "generic.deleteFailed": "Delete Failed", "generic.deleted": "Deleted", @@ -2178,4 +2182,4 @@ "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", "recodex-judge-shuffle-rows": "Unordered-rows judge" -} +} \ No newline at end of file diff --git a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js index 7e4177356..184dc4b30 100644 --- a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js +++ b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js @@ -27,6 +27,7 @@ import ReviewSummary from '../../components/Solutions/ReviewSummary'; import RecentlyVisited from '../../components/Solutions/RecentlyVisited'; import { registerSolutionVisit } from '../../components/Solutions/RecentlyVisited/functions.js'; import Callout from '../../components/widgets/Callout'; +import { localStorageHighlightOverridesKey } from '../../components/helpers/SourceCodeViewer'; import SolutionActionsContainer from '../../containers/SolutionActionsContainer'; import SolutionReviewRequestButtonContainer from '../../containers/SolutionReviewRequestButtonContainer'; import CommentThreadContainer from '../../containers/CommentThreadContainer'; @@ -58,12 +59,11 @@ import { loggedUserIsPrimaryAdminOfSelector } from '../../redux/selectors/usersG import { storageGetItem, storageSetItem, storageRemoveItem } from '../../helpers/localStorage.js'; import { isSupervisorRole } from '../../components/helpers/usersRoles.js'; -import { hasPermissions, hasOneOfPermissions, isEmptyObject, EMPTY_ARRAY } from '../../helpers/common.js'; -import { preprocessFiles, associateFilesForDiff, getRevertedMapping, groupReviewCommentPerFile } from './functions.js'; - import { isStudentLocked } from '../../components/helpers/exams.js'; import withLinks from '../../helpers/withLinks.js'; import withRouter, { withRouterProps } from '../../helpers/withRouter.js'; +import { hasPermissions, hasOneOfPermissions, isEmptyObject, EMPTY_ARRAY } from '../../helpers/common.js'; +import { preprocessFiles, associateFilesForDiff, getRevertedMapping, groupReviewCommentPerFile } from './functions.js'; const fileNameAndEntry = file => [file.parentId || file.id, file.entryName || null]; @@ -78,6 +78,7 @@ class SolutionSourceCodes extends Component { mappingDialogDiffWith: null, diffMappings: {}, commentsOpen: false, + highlightOverrides: {}, }; static loadAsync = ({ solutionId, assignmentId, secondSolutionId }, dispatch) => @@ -122,10 +123,12 @@ class SolutionSourceCodes extends Component { const lsKey = this.getDiffMappingsLocalStorageKey(); if (lsKey) { - this.setState({ - diffMappings: storageGetItem(lsKey, {}), - }); + this.setState({ diffMappings: storageGetItem(lsKey, {}) }); } + + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } componentDidUpdate(prevProps) { @@ -137,10 +140,12 @@ class SolutionSourceCodes extends Component { const lsKey = this.getDiffMappingsLocalStorageKey(); if (lsKey) { - this.setState({ - diffMappings: storageGetItem(lsKey, {}), - }); + this.setState({ diffMappings: storageGetItem(lsKey, {}) }); } + + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } } @@ -213,6 +218,15 @@ class SolutionSourceCodes extends Component { } }; + setHighlightOverride = (extension, mode) => { + const { [extension]: _, ...newOverrides } = this.state.highlightOverrides; + if (mode || mode === '') { + newOverrides[extension] = mode; + } + this.setState({ highlightOverrides: newOverrides }); + storageSetItem(localStorageHighlightOverridesKey, newOverrides); + }; + render() { const { assignment, @@ -520,6 +534,8 @@ class SolutionSourceCodes extends Component { removeComment={ canUpdateComments && hasPermissions(solution, 'review') ? removeComment : null } + highlightOverrides={this.state.highlightOverrides} + setHighlightOverride={this.setHighlightOverride} /> ))}