From 2184ed45a7e011e8379031ec99a23ddd8a499bfb Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Sat, 29 Jul 2023 16:56:38 +0200 Subject: [PATCH] Using a double click as a shortcut to open item details in item lists (exercises, assignments, solutions...). This is opt-in, configurable in user UI settings. --- .../AssignmentTableRow/AssignmentTableRow.js | 12 +- .../AssignmentsTable/AssignmentsTable.js | 436 +++++++++--------- .../ShadowAssignmentsTable.js | 148 +++--- .../ShadowAssignmentsTableRow.js | 4 +- .../SolutionsTable/SolutionsTable.js | 215 +++++---- .../SolutionsTable/SolutionsTableRow.js | 8 +- .../Exercises/ExercisesList/ExercisesList.js | 75 +-- .../ExercisesListItem/ExercisesListItem.js | 5 +- .../SolutionStatus/SolutionStatus.js | 2 +- .../EditUserProfileForm.js | 5 +- .../EditUserUIDataForm/EditUserUIDataForm.js | 21 + src/components/widgets/Comments/comments.less | 2 +- .../widgets/SortableTable/SortableTable.js | 146 +++--- .../PaginationContainer.js | 3 +- src/locales/cs.json | 4 +- src/locales/en.json | 4 +- .../AssignmentSolutions.js | 9 + src/pages/EditUser/EditUser.js | 2 + .../GroupUserSolutions/GroupUserSolutions.js | 7 + 19 files changed, 633 insertions(+), 475 deletions(-) diff --git a/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js b/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js index 3904e42c1..38c7ab7cf 100644 --- a/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js +++ b/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js @@ -48,6 +48,7 @@ const AssignmentTableRow = ({ discussionOpen, setSelected = null, selected = false, + doubleClickPush = null, intl: { locale }, links: { ASSIGNMENT_DETAIL_URI_FACTORY, @@ -57,7 +58,15 @@ const AssignmentTableRow = ({ GROUP_ASSIGNMENTS_URI_FACTORY, }, }) => ( - + + doubleClickPush( + userId ? ASSIGNMENT_DETAIL_SPECIFIC_USER_URI_FACTORY(id, userId) : ASSIGNMENT_DETAIL_URI_FACTORY(id) + )) + }> {setSelected && ( @@ -219,6 +228,7 @@ AssignmentTableRow.propTypes = { showSecondDeadline: PropTypes.bool, setSelected: PropTypes.func, selected: PropTypes.bool, + doubleClickPush: PropTypes.func, groupsAccessor: PropTypes.func, discussionOpen: PropTypes.func, links: PropTypes.object, diff --git a/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js b/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js index 495d16f8d..f87a9cd92 100644 --- a/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js +++ b/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Table, Modal } from 'react-bootstrap'; import { FormattedMessage, injectIntl } from 'react-intl'; +import { withRouter } from 'react-router'; import { defaultMemoize } from 'reselect'; import moment from 'moment'; @@ -13,6 +14,7 @@ import Icon, { DeleteIcon, InvertIcon, LoadingIcon, RefreshIcon, SquareIcon, Vis import Button, { TheButtonGroup } from '../../../widgets/TheButton'; import { compareAssignmentsReverted, isBeforeDeadline, isUpToDate } from '../../../helpers/assignments'; import { LocalizedExerciseName } from '../../../helpers/LocalizedNames'; +import { UserUIDataContext } from '../../../../helpers/contexts'; import { EMPTY_LIST, EMPTY_OBJ, EMPTY_ARRAY } from '../../../../helpers/common'; import { prepareInitialValues, transformSubmittedData } from '../../../forms/EditAssignmentForm'; @@ -180,6 +182,7 @@ class AssignmentsTable extends Component { editAssignment = null, deleteAssignment = null, intl: { locale }, + history: { push }, } = this.props; const someAssignmentsAreLoading = assignments.some(isLoading); const assignmentsPreprocessedAll = assignments @@ -196,227 +199,243 @@ class AssignmentsTable extends Component { const multiActions = Boolean(syncAssignment || editAssignment || deleteAssignment); return ( <> - - {assignmentsPreprocessed.length > 0 && ( - - - {multiActions && - )} + + {({ openOnDoubleclick = false }) => ( +
} - - - {showNames && ( - - -
+ {assignmentsPreprocessed.length > 0 && ( + + + {multiActions && + )} - {showGroups && groupsAccessor && ( - - )} + {showGroups && groupsAccessor && ( + + )} - {assignmentEnvironmentsSelector && ( - - )} + {assignmentEnvironmentsSelector && ( + + )} - {!isAdmin && Object.keys(stats).length !== 0 && ( - - )} + {!isAdmin && Object.keys(stats).length !== 0 && ( + + )} - + - {showSecondDeadline && ( - + {showSecondDeadline && ( + + )} + + + + + + )} + + {someAssignmentsAreLoading ? ( + + ) : ( + assignmentsPreprocessedAll.length === 0 && ( + + + + ) )} - + {!someAssignmentsAreLoading && + assignmentsPreprocessed.map(assignment => ( + item.id === assignment.id) + : null + } + isAdmin={isAdmin} + showNames={showNames} + showGroups={showGroups} + showSecondDeadline={showSecondDeadline} + groupsAccessor={groupsAccessor} + discussionOpen={() => this.openDialog(assignment)} + setSelected={multiActions ? this.selectAssignmentClickHandler(assignmentsPreprocessedAll) : null} + selected={Boolean(this.state.selectedAssignments[assignment.id])} + doubleClickPush={openOnDoubleclick ? push : null} + /> + ))} + + + {!someAssignmentsAreLoading && + onlyCurrent && + assignmentsPreprocessedAll.length !== assignmentsPreprocessedCurrent.length && ( + + + + + + )} - - - )} - - {someAssignmentsAreLoading ? ( - - ) : ( - assignmentsPreprocessedAll.length === 0 && ( - - - - ) - )} - - {!someAssignmentsAreLoading && - assignmentsPreprocessed.map(assignment => ( - item.id === assignment.id) : null - } - isAdmin={isAdmin} - showNames={showNames} - showGroups={showGroups} - showSecondDeadline={showSecondDeadline} - groupsAccessor={groupsAccessor} - discussionOpen={() => this.openDialog(assignment)} - setSelected={multiActions ? this.selectAssignmentClickHandler(assignmentsPreprocessedAll) : null} - selected={Boolean(this.state.selectedAssignments[assignment.id])} - /> - ))} - - - {!someAssignmentsAreLoading && - onlyCurrent && - assignmentsPreprocessedAll.length !== assignmentsPreprocessedCurrent.length && ( - - - + + - - - )} - - {multiActions && !someAssignmentsAreLoading && assignmentsPreprocessed.length > 0 && ( - - - - - + {deleteAssignment && ( + + )} + + + + + )} +
} + + + {showNames && ( + + + - - + + - - + + - - + + - - + + - - + + + + +
+ {showGroups ? ( + + ) : ( + + )} +
- -
+ {this.state.showAll ? ( + ( + + {content} + + ), + }} + /> + ) : ( + ( + + {content} + + ), + }} + /> + )} +
-
- {showGroups ? ( - - ) : ( - - )} -
- {this.state.showAll ? ( - ( - - {content} - - ), - }} - /> - ) : ( - ( - - {content} - - ), - }} + {multiActions && !someAssignmentsAreLoading && assignmentsPreprocessed.length > 0 && ( +
+ - )} -
- - - - - - - - {syncAssignment && ( - - )} + + + + + + + {syncAssignment && ( + + )} - {editAssignment && ( - - )} + {editAssignment && ( + + )} - {editAssignment && ( - )} - - - )} - {deleteAssignment && ( - - )} - -
)} - + {this.state.dialogAssignment && ( @@ -460,6 +479,9 @@ AssignmentsTable.propTypes = { editAssignment: PropTypes.func, deleteAssignment: PropTypes.func, intl: PropTypes.object.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }), }; -export default injectIntl(AssignmentsTable); +export default withRouter(injectIntl(AssignmentsTable)); diff --git a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js index 853bb1a3e..f6e974091 100644 --- a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js +++ b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js @@ -2,12 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Table } from 'react-bootstrap'; +import { withRouter } from 'react-router'; import { FormattedMessage, injectIntl } from 'react-intl'; import { isReady, isLoading, getJsData } from '../../../../redux/helpers/resourceManager'; import ShadowAssignmentsTableRow from './ShadowAssignmentsTableRow'; import { compareShadowAssignments } from '../../../helpers/assignments'; import { LoadingIcon } from '../../../icons'; +import { UserUIDataContext } from '../../../../helpers/contexts'; import { EMPTY_LIST, EMPTY_OBJ } from '../../../../helpers/common'; const ShadowAssignmentsTable = ({ @@ -16,77 +18,88 @@ const ShadowAssignmentsTable = ({ stats = EMPTY_OBJ, isAdmin = false, intl: { locale }, + history: { push }, }) => ( - 0} className="mb-0"> - {shadowAssignments.size > 0 && ( - - - - - + + {({ openOnDoubleclick = false }) => ( +
- - - - - - -
0} className="mb-0"> + {shadowAssignments.size > 0 && ( + + + + + - + - {!isAdmin && ( - - )} + {!isAdmin && ( + + )} - - - )} - - {shadowAssignments.size === 0 && ( - - - - )} + + + )} + + {shadowAssignments.size === 0 && ( + + + + )} - {shadowAssignments.some(isLoading) && ( - - - - )} + {shadowAssignments.some(isLoading) && ( + + + + )} - {shadowAssignments - .filter(isReady) - .map(getJsData) - .sort(compareShadowAssignments) - .map(assignment => ( - item.id === assignment.id) : null} - isAdmin={isAdmin} - /> - ))} - -
+ + + + + + + - {!isAdmin ? ( - - ) : ( - - )} - + {!isAdmin ? ( + + ) : ( + + )} + - - + + -
- -
+
+ +
- - -
+ + +
+ {shadowAssignments + .filter(isReady) + .map(getJsData) + .sort(compareShadowAssignments) + .map(assignment => ( + item.id === assignment.id) : null + } + isAdmin={isAdmin} + doubleClickPush={openOnDoubleclick ? push : null} + /> + ))} + + + )} + ); ShadowAssignmentsTable.propTypes = { @@ -95,6 +108,9 @@ ShadowAssignmentsTable.propTypes = { stats: PropTypes.object, isAdmin: PropTypes.bool, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }), }; -export default injectIntl(ShadowAssignmentsTable); +export default withRouter(injectIntl(ShadowAssignmentsTable)); diff --git a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTableRow.js b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTableRow.js index 6bf31957b..a09daed3b 100644 --- a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTableRow.js +++ b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTableRow.js @@ -24,9 +24,10 @@ const ShadowAssignmentsTableRow = ({ item: { id, localizedTexts, createdAt, deadline, isBonus, isPublic, maxPoints, points, permissionHints }, userId, isAdmin, + doubleClickPush, links: { SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY, SHADOW_ASSIGNMENT_EDIT_URI_FACTORY }, }) => ( - + doubleClickPush(SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY(id)))}> {permissionHints.update && } @@ -100,6 +101,7 @@ ShadowAssignmentsTableRow.propTypes = { userId: PropTypes.string, links: PropTypes.object, isAdmin: PropTypes.bool, + doubleClickPush: PropTypes.func, }; export default withLinks(ShadowAssignmentsTableRow); diff --git a/src/components/Assignments/SolutionsTable/SolutionsTable.js b/src/components/Assignments/SolutionsTable/SolutionsTable.js index f991ae3a5..e5c308582 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTable.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTable.js @@ -3,12 +3,14 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import { Table } from 'react-bootstrap'; +import { withRouter } from 'react-router'; import { defaultMemoize } from 'reselect'; import NoSolutionYetTableRow from './NoSolutionYetTableRow'; import SolutionsTableRow from './SolutionsTableRow'; import { LoadingIcon } from '../../icons'; import { EMPTY_ARRAY } from '../../../helpers/common'; +import { UserUIDataContext } from '../../../helpers/contexts'; import styles from './SolutionsTable.less'; @@ -27,121 +29,127 @@ const SolutionsTable = ({ assignmentSolversLoading = false, showActionButtons = true, onSelect = null, + history: { push }, }) => { const highlightsIndex = createHighlightsIndex(highlights); return ( - - - - - - - + + {({ openOnDoubleclick = false }) => ( +
- - - - - - - - - -
+ + + + + + - {!compact && ( - - )} + {!compact && ( + + )} - {(!compact || showActionButtons) && ( - )} - - )} - - - {solutions.size === 0 ? ( - - ) : ( - solutions.map((data, idx) => { - if (!data) { - return ( - - - - - - ); - } + + + {solutions.size === 0 ? ( + + ) : ( + solutions.map((data, idx) => { + if (!data) { + return ( + + + + + + ); + } - const id = data.id; - const runtimeEnvironment = - data.runtimeEnvironmentId && - runtimeEnvironments && - runtimeEnvironments.find(({ id }) => id === data.runtimeEnvironmentId); + const id = data.id; + const runtimeEnvironment = + data.runtimeEnvironmentId && + runtimeEnvironments && + runtimeEnvironments.find(({ id }) => id === data.runtimeEnvironmentId); - return ( - - ); - }) + return ( + + ); + }) + )} +
+ + + + + + + + + + - - + + - {assignmentSolversLoading ? ( - - ) : ( - <> - {assignmentSolver && - (assignmentSolver.get('lastAttemptIndex') > 5 || - assignmentSolver.get('lastAttemptIndex') > solutions.size) && ( - <> - {!compact && ( - - )} + {(!compact || showActionButtons) && ( + + {assignmentSolversLoading ? ( + + ) : ( + <> + {assignmentSolver && + (assignmentSolver.get('lastAttemptIndex') > 5 || + assignmentSolver.get('lastAttemptIndex') > solutions.size) && ( + <> + {!compact && ( + + )} - {assignmentSolver.get('lastAttemptIndex') > solutions.size && ( - - {!compact && <>  }( - - ) - + {assignmentSolver.get('lastAttemptIndex') > solutions.size && ( + + {!compact && <>  }( + + ) + + )} + )} - - )} - {!compact && !assignmentSolver && solutions.size > 5 && ( - + {!compact && !assignmentSolver && solutions.size > 5 && ( + + )} + )} - +
- -
+ +
)} - + ); }; @@ -158,6 +166,9 @@ SolutionsTable.propTypes = { assignmentSolversLoading: PropTypes.bool, showActionButtons: PropTypes.bool, onSelect: PropTypes.func, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }), }; -export default SolutionsTable; +export default withRouter(SolutionsTable); diff --git a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js index 08e7ea762..3da62b16a 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js @@ -45,6 +45,7 @@ const SolutionsTableRow = ({ selected = false, highlighted = false, showActionButtons = true, + doubleclickAction = null, onSelect = null, links: { SOLUTION_DETAIL_URI_FACTORY, SOLUTION_SOURCE_CODES_URI_FACTORY }, intl: { locale }, @@ -62,7 +63,11 @@ const SolutionsTableRow = ({ const splitOnTwoLines = hasNote && compact; return ( - onSelect(id) : null}> + onSelect(id) : null} + onDoubleClick={ + !onSelect && doubleclickAction && (() => doubleclickAction(SOLUTION_DETAIL_URI_FACTORY(assignmentId, id))) + }> ( - - {Boolean(heading) && {heading}} - - {exercises.map((exercise, idx) => - exercise ? ( - - ) : ( - - - - ) - )} + + {({ openOnDoubleclick = false }) => ( +
- - -
+ {Boolean(heading) && {heading}} + + {exercises.map((exercise, idx) => + exercise ? ( + + ) : ( + + + + ) + )} - {exercises.length === 0 && ( - - - - )} - -
+ + +
- -
+ {exercises.length === 0 && ( + + + + + + )} + + + )} + ); ExercisesList.propTypes = { @@ -55,6 +63,9 @@ ExercisesList.propTypes = { showAssignButton: PropTypes.bool, assignExercise: PropTypes.func, reload: PropTypes.func, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }), }; -export default ExercisesList; +export default withRouter(ExercisesList); diff --git a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js index e6358b084..52f9634d6 100644 --- a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js +++ b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js @@ -39,6 +39,7 @@ const ExercisesListItem = ({ showAssignButton = false, assignExercise = null, reload, + doubleClickPush = null, links: { EXERCISE_URI_FACTORY, EXERCISE_EDIT_URI_FACTORY, @@ -46,7 +47,8 @@ const ExercisesListItem = ({ EXERCISE_EDIT_LIMITS_URI_FACTORY, }, }) => ( - + doubleClickPush(EXERCISE_URI_FACTORY(id)))}> )} - {review.closedAt && ( + {review && review.closedAt && ( } /> ) @@ -202,7 +202,7 @@ const EditUserProfileForm = ({ } /> @@ -216,6 +216,7 @@ const EditUserProfileForm = ({ name="passwordConfirm" tabIndex={8} component={PasswordField} + autoComplete="new-password" label={} /> diff --git a/src/components/forms/EditUserUIDataForm/EditUserUIDataForm.js b/src/components/forms/EditUserUIDataForm/EditUserUIDataForm.js index 79da56620..c479f9edc 100644 --- a/src/components/forms/EditUserUIDataForm/EditUserUIDataForm.js +++ b/src/components/forms/EditUserUIDataForm/EditUserUIDataForm.js @@ -7,6 +7,7 @@ import { defaultMemoize } from 'reselect'; import Callout from '../../widgets/Callout'; import FormBox from '../../widgets/FormBox'; import { SaveIcon } from '../../icons'; +import Explanation from '../../widgets/Explanation'; import SubmitButton from '../SubmitButton'; import { CheckboxField, SelectField, NumericTextField } from '../Fields'; @@ -111,6 +112,26 @@ const EditUserUIDataForm = ({ } /> + + + + + + + } + /> + ( - - {columns.map(({ id: colId, cellRenderer, style, className, onClick, isClickable }) => { - if (typeof isClickable === 'function') { - isClickable = isClickable(row.id, colId); - } - return ( - onClick(row.id, colId) : null}> - {cellRenderer(row[colId], idx, colId, row)} - - ); - })} - - ); + defaultRowRenderer = (row, idx, columns, openLinkGenerator = null) => { + const { + history: { push }, + } = this.props; + const doubleClickLink = openLinkGenerator && openLinkGenerator(row, idx); + + return ( + push(doubleClickLink))}> + {columns.map(({ id: colId, cellRenderer, style, className, onClick, isClickable }) => { + if (typeof isClickable === 'function') { + isClickable = isClickable(row.id, colId); + } + return ( + onClick(row.id, colId) : null}> + {cellRenderer(row[colId], idx, colId, row)} + + ); + })} + + ); + }; // Change internal state that holds sorting parameters. orderBy = colId => { @@ -81,52 +94,66 @@ class SortableTable extends Component { data = [], empty = null, rowRenderer = this.defaultRowRenderer, + openLinkGenerator = null, + staticContext /* avoid capturing static context in the rest of ...props */, ...props } = this.props; const { sortColumn, ascendant } = this.state; return ( - - {columns.length > 0 && ( - - - {columns.map(column => ( - - {data.length > 0 ? ( - this.sortData(data, sortColumn, ascendant).map((row, idx) => rowRenderer(row, idx, columns)) - ) : ( - - - - )} - -
- {column.header} - {column.comparator && data.length > 1 && ( - - this.orderBy(column.id)} - /> + + {({ openOnDoubleclick = false }) => ( + + {columns.length > 0 && ( + + + {columns.map(column => ( + - ))} - - {this.getHeaderSuffixRow()} - + + ))} + + {this.getHeaderSuffixRow()} + + )} + + {data.length > 0 ? ( + this.sortData(data, sortColumn, ascendant).map((row, idx) => + rowRenderer(row, idx, columns, openOnDoubleclick ? openLinkGenerator : null) + ) + ) : ( + + + + )} + +
+ {column.header} + {column.comparator && data.length > 1 && ( + + this.orderBy(column.id)} + /> - {sortColumn === column.id && !defaultOrder && ( - this.orderBy(null)} /> + {sortColumn === column.id && !defaultOrder && ( + this.orderBy(null)} /> + )} + )} - - )} -
+ {empty || ( + + )} +
)} -
- {empty || ( - - )} -
+ ); } } @@ -137,6 +164,11 @@ SortableTable.propTypes = { data: PropTypes.array, empty: PropTypes.any, rowRenderer: PropTypes.func, + openLinkGenerator: PropTypes.func, + staticContext: PropTypes.any, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }), }; -export default SortableTable; +export default withRouter(SortableTable); diff --git a/src/containers/PaginationContainer/PaginationContainer.js b/src/containers/PaginationContainer/PaginationContainer.js index 4112fb993..ca0c9df2e 100644 --- a/src/containers/PaginationContainer/PaginationContainer.js +++ b/src/containers/PaginationContainer/PaginationContainer.js @@ -50,11 +50,12 @@ export const showRangeInfo = (offset, limit, totalCount) =>
 , }} />
diff --git a/src/locales/cs.json b/src/locales/cs.json index df4963eb2..0513c1a06 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -660,6 +660,8 @@ "app.editUserUIData.editorFontSize": "Velikost fontu v textovém editoru:", "app.editUserUIData.failed": "Nebylo možné uložit nastavení vzhledu.", "app.editUserUIData.lastNameFirst": "V seznamech zobrazovat uživatelům nejprve příjmení", + "app.editUserUIData.openOnDoubleclick": "V seznamech používat dvojitý klik k otevření detailu položky", + "app.editUserUIData.openOnDoubleclick.explain": "U seznamů, které zobrazují u položek tlačítko nebo odkaz na detail položky, bude možné použít dvojitý klik (kamkoli na celý řádek) jako rychlou alternativu k danému tlačítku nebo odkazu.", "app.editUserUIData.openedSidebar": "Postraní panel je ve výchozím stavu rozbalený", "app.editUserUIData.title": "Nastavení vzhledu", "app.editUserUIData.useGravatar": "Používat službu Gravatar pro zobrazování avatarů ostatních uživatelů", @@ -1174,7 +1176,7 @@ "app.page.failedDescription.explain": "Tento problém mohl být způsoben výpadkem sítě nebo interní chybou na straně serveru. Rovněž je možné, že požadované datové objekty pro zobrazení této stránky byly smazány.", "app.page.failedDescription.sorry": "Prosíme, zkuste to později. Omlouváme se za způsobené problémy. Pokud problém přetrvává ověřte, že zobrazovaný objekt stále existuje.", "app.page.loadingDescription": "Prosíme počkejte než bude vše připraveno.", - "app.paginationContainer.showingRange": "zobrazuje se {offset}. - {offsetEnd}. (z {totalCount})", + "app.paginationContainer.showingRange": "zobrazuje se {offset}.{nbsp}-{nbsp}{offsetEnd}. (z{nbsp}{totalCount})", "app.passwordStrength.bad": "Víte to jistě?", "app.passwordStrength.good": "Dobře", "app.passwordStrength.ok": "OK", diff --git a/src/locales/en.json b/src/locales/en.json index 4042700a5..1b139ea89 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -660,6 +660,8 @@ "app.editUserUIData.editorFontSize": "Code editor font size:", "app.editUserUIData.failed": "Cannot save visual settings of the user.", "app.editUserUIData.lastNameFirst": "In listings, show last names of users first", + "app.editUserUIData.openOnDoubleclick": "In listings, use double click to open item details", + "app.editUserUIData.openOnDoubleclick.explain": "For the listing that show items with buttons or links leading to item detials, a double click (anywhere on the listing row) will be used as a shortcut for that button or link.", "app.editUserUIData.openedSidebar": "Sidebar is unfolded by default", "app.editUserUIData.title": "Visual Settings", "app.editUserUIData.useGravatar": "Use Gravatar service for fetching user avatars", @@ -1174,7 +1176,7 @@ "app.page.failedDescription.explain": "This problem might have been caused by network failure or by internal error at server side. It is also possible that some of the resources required for displaying this page have been deleted.", "app.page.failedDescription.sorry": "We are sorry for the inconvenience, please try again later. If the problem prevails, verify that the requested resource still exists.", "app.page.loadingDescription": "Please wait while we are getting things ready.", - "app.paginationContainer.showingRange": "showing {offset} - {offsetEnd} (of {totalCount})", + "app.paginationContainer.showingRange": "showing {offset}{nbsp}-{nbsp}{offsetEnd} (of{nbsp}{totalCount})", "app.passwordStrength.bad": "Are you sure?", "app.passwordStrength.good": "Good", "app.passwordStrength.ok": "OK", diff --git a/src/pages/AssignmentSolutions/AssignmentSolutions.js b/src/pages/AssignmentSolutions/AssignmentSolutions.js index c43a1a40a..37299d863 100644 --- a/src/pages/AssignmentSolutions/AssignmentSolutions.js +++ b/src/pages/AssignmentSolutions/AssignmentSolutions.js @@ -410,6 +410,14 @@ class AssignmentSolutions extends Component { ); }; + openLinkGenerator = ({ actionButtons: { id, permissionHints } }) => { + const { + assignmentId, + links: { SOLUTION_DETAIL_URI_FACTORY }, + } = this.props; + return permissionHints && permissionHints.viewDetail ? SOLUTION_DETAIL_URI_FACTORY(assignmentId, id) : null; + }; + // Re-format the data, so they can be rendered by the SortableTable ... render() { const { @@ -647,6 +655,7 @@ class AssignmentSolutions extends Component { runtimes, this.state.viewMode )} + openLinkGenerator={this.openLinkGenerator} empty={
{ + const { + links: { SOLUTION_DETAIL_URI_FACTORY }, + } = this.props; + return permissionHints && permissionHints.viewDetail ? SOLUTION_DETAIL_URI_FACTORY(assignmentId, id) : null; + }; + // Re-format the data, so they can be rendered by the SortableTable ... render() { const {