From 2a39e0e058a6de848a6a42857c6d12715a950b76 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Tue, 19 Nov 2019 00:28:24 +0100 Subject: [PATCH] Adding tags to exercise details and exercise lists. Adjusting appearance of the list to make room for tags (and some refactoring). --- .../DifficultyIcon/DifficultyIcon.js | 53 +++++++-------- .../ExerciseButtons/ExerciseButtons.js | 10 +-- .../ExerciseDetail/ExerciseDetail.js | 23 ++++++- .../ExercisesListItem/ExercisesListItem.js | 68 +++++++++++++++---- .../AssignExerciseButton.js | 2 +- .../DeleteButton/ConfirmDeleteButton.js | 12 +--- .../buttons/DeleteButton/DeleteButtonRaw.js | 35 ++++++++++ src/components/buttons/DeleteButton/index.js | 1 + .../EnvironmentsList/EnvironmentsList.less | 3 - .../EnvironmentsList/EnvironmentsListItem.js | 3 +- src/components/icons/index.js | 3 + src/containers/App/recodex.css | 5 ++ .../ExercisesListContainer.js | 3 + .../ExercisesTagsEditContainer.js | 4 +- src/helpers/exercise/tags.js | 4 +- src/locales/cs.json | 10 +-- src/locales/en.json | 4 +- src/locales/whitelist_en.json | 2 + 18 files changed, 172 insertions(+), 73 deletions(-) create mode 100644 src/components/buttons/DeleteButton/DeleteButtonRaw.js delete mode 100644 src/components/helpers/EnvironmentsList/EnvironmentsList.less diff --git a/src/components/Exercises/DifficultyIcon/DifficultyIcon.js b/src/components/Exercises/DifficultyIcon/DifficultyIcon.js index d8cbd59fd..77c9c6134 100644 --- a/src/components/Exercises/DifficultyIcon/DifficultyIcon.js +++ b/src/components/Exercises/DifficultyIcon/DifficultyIcon.js @@ -3,38 +3,35 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import Icon from '../../icons'; -const DifficultyIcon = ({ difficulty }) => { - switch (difficulty) { - case 'easy': - return ( - - - - - ); - - case 'medium': - case 'moderate': - return ( - - - - - ); +const difficultyIcons = { + easy: 'smile', + medium: 'meh', + hard: 'frown', +}; - case 'hard': - return ( - - - - - ); +const difficultyClassNames = { + easy: 'text-success', + medium: 'text-warning', + hard: 'text-danger', +}; - default: - return null; - } +const difficultyCaptions = { + easy: , + medium: , + hard: , }; +const DifficultyIcon = ({ difficulty }) => ( + + + + {difficultyCaptions[difficulty] || ( + + )} + + +); + DifficultyIcon.propTypes = { difficulty: PropTypes.string.isRequired, }; diff --git a/src/components/Exercises/ExerciseButtons/ExerciseButtons.js b/src/components/Exercises/ExerciseButtons/ExerciseButtons.js index 04679fd8e..d0b3b4955 100644 --- a/src/components/Exercises/ExerciseButtons/ExerciseButtons.js +++ b/src/components/Exercises/ExerciseButtons/ExerciseButtons.js @@ -5,7 +5,7 @@ import { ButtonGroup } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import Button from '../../widgets/FlatButton'; -import Icon, { EditIcon } from '../../icons'; +import { EditIcon, GroupIcon, LimitsIcon, TestsIcon } from '../../icons'; import withLinks from '../../../helpers/withLinks'; const ExerciseButtons = ({ @@ -22,8 +22,8 @@ const ExerciseButtons = ({ @@ -39,7 +39,7 @@ const ExerciseButtons = ({ {permissionHints && permissionHints.viewPipelines && permissionHints.viewScoreConfig && ( @@ -48,7 +48,7 @@ const ExerciseButtons = ({ {permissionHints && permissionHints.viewLimits && ( diff --git a/src/components/Exercises/ExerciseDetail/ExerciseDetail.js b/src/components/Exercises/ExerciseDetail/ExerciseDetail.js index 665ae8369..f65c4c812 100644 --- a/src/components/Exercises/ExerciseDetail/ExerciseDetail.js +++ b/src/components/Exercises/ExerciseDetail/ExerciseDetail.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, FormattedNumber } from 'react-intl'; -import { Table } from 'react-bootstrap'; +import { Table, Label } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import Box from '../../widgets/Box'; @@ -11,11 +11,12 @@ import DifficultyIcon from '../DifficultyIcon'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import withLinks from '../../../helpers/withLinks'; import UsersNameContainer from '../../../containers/UsersNameContainer'; -import Icon, { SuccessOrFailureIcon, UserIcon, VisibleIcon, CodeIcon } from '../../icons'; +import Icon, { SuccessOrFailureIcon, UserIcon, VisibleIcon, CodeIcon, TagIcon } from '../../icons'; import { getLocalizedDescription } from '../../../helpers/localizedData'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import EnvironmentsList from '../../helpers/EnvironmentsList'; import Version from '../../widgets/Version/Version'; +import { getTagStyle } from '../../../helpers/exercise/tags'; const ExerciseDetail = ({ authorId, @@ -27,6 +28,7 @@ const ExerciseDetail = ({ forkedFrom = null, localizedTexts, runtimeEnvironments, + tags, isPublic, isLocked, locale, @@ -91,6 +93,22 @@ const ExerciseDetail = ({ + + + + + + : + + + {tags.sort().map(tag => ( + + ))} + + + @@ -180,6 +198,7 @@ ExerciseDetail.propTypes = { forkedFrom: PropTypes.object, localizedTexts: PropTypes.array.isRequired, runtimeEnvironments: PropTypes.array.isRequired, + tags: PropTypes.array.isRequired, isPublic: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired, locale: PropTypes.string.isRequired, diff --git a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js index a5b1f5b07..8b870adbd 100644 --- a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js +++ b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { LinkContainer } from 'react-router-bootstrap'; import { Link } from 'react-router-dom'; +import { Label, OverlayTrigger, Tooltip } from 'react-bootstrap'; import DifficultyIcon from '../DifficultyIcon'; import UsersNameContainer from '../../../containers/UsersNameContainer'; @@ -11,10 +12,11 @@ import DeleteExerciseButtonContainer from '../../../containers/DeleteExerciseBut import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import EnvironmentsList from '../../helpers/EnvironmentsList'; -import { ExercisePrefixIcons, EditIcon } from '../../icons'; +import { ExercisePrefixIcons, EditIcon, LimitsIcon, TestsIcon } from '../../icons'; import Button from '../../widgets/FlatButton'; import DateTime from '../../widgets/DateTime'; import AssignExerciseButton from '../../buttons/AssignExerciseButton'; +import { getTagStyle } from '../../../helpers/exercise/tags'; import withLinks from '../../../helpers/withLinks'; @@ -24,6 +26,7 @@ const ExercisesListItem = ({ difficulty, authorId, runtimeEnvironments, + tags, groupsIds = [], localizedTexts, createdAt, @@ -46,6 +49,7 @@ const ExercisesListItem = ({ + {permissionHints.viewDetail ? ( @@ -57,10 +61,21 @@ const ExercisesListItem = ({ )} + + {runtimeEnvironments && } + + + {tags.sort().map(tag => ( + + ))} + + {showGroups && ( {groupsIds.length > 0 ? ( @@ -76,13 +91,16 @@ const ExercisesListItem = ({ )} )} + + @@ -102,30 +120,51 @@ const ExercisesListItem = ({ )} {permissionHints.update && ( - + + + + }> + + )} {permissionHints.viewPipelines && permissionHints.viewScoreConfig && ( - + + + + }> + + )} {permissionHints.viewLimits && ( - + + + + }> + + )} {permissionHints.remove && ( - + )} @@ -135,6 +174,7 @@ ExercisesListItem.propTypes = { id: PropTypes.string.isRequired, authorId: PropTypes.string.isRequired, runtimeEnvironments: PropTypes.array.isRequired, + tags: PropTypes.array.isRequired, groupsIds: PropTypes.array, name: PropTypes.string.isRequired, difficulty: PropTypes.string.isRequired, diff --git a/src/components/buttons/AssignExerciseButton/AssignExerciseButton.js b/src/components/buttons/AssignExerciseButton/AssignExerciseButton.js index 00baf68ec..5a92b5fad 100644 --- a/src/components/buttons/AssignExerciseButton/AssignExerciseButton.js +++ b/src/components/buttons/AssignExerciseButton/AssignExerciseButton.js @@ -18,7 +18,7 @@ const AssignExerciseButton = ({ isLocked, isBroken, assignExercise, ...props }) ); } else { return ( - diff --git a/src/components/buttons/DeleteButton/ConfirmDeleteButton.js b/src/components/buttons/DeleteButton/ConfirmDeleteButton.js index add50dac3..a6156088a 100644 --- a/src/components/buttons/DeleteButton/ConfirmDeleteButton.js +++ b/src/components/buttons/DeleteButton/ConfirmDeleteButton.js @@ -1,15 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import Button from '../../widgets/FlatButton'; -import { DeleteIcon } from '../../icons'; import Confirm from '../../forms/Confirm'; +import DeleteButtonRaw from './DeleteButtonRaw'; const ConfirmDeleteButton = ({ id, onClick, - disabled, - small = true, question = ( ( - + ); ConfirmDeleteButton.propTypes = { onClick: PropTypes.func, id: PropTypes.string, - small: PropTypes.bool, question: PropTypes.any, - disabled: PropTypes.bool, }; export default ConfirmDeleteButton; diff --git a/src/components/buttons/DeleteButton/DeleteButtonRaw.js b/src/components/buttons/DeleteButton/DeleteButtonRaw.js new file mode 100644 index 000000000..02d8eed89 --- /dev/null +++ b/src/components/buttons/DeleteButton/DeleteButtonRaw.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import Button from '../../widgets/FlatButton'; +import { DeleteIcon } from '../../icons'; + +const DeleteButtonRaw = ({ id, disabled, small = true, captionAsLabel = false, ...props }) => + captionAsLabel ? ( + + + + }> + + + ) : ( + + ); + +DeleteButtonRaw.propTypes = { + id: PropTypes.string, + small: PropTypes.bool, + captionAsLabel: PropTypes.bool, + disabled: PropTypes.bool, +}; + +export default DeleteButtonRaw; diff --git a/src/components/buttons/DeleteButton/index.js b/src/components/buttons/DeleteButton/index.js index 1833cd586..d150063be 100644 --- a/src/components/buttons/DeleteButton/index.js +++ b/src/components/buttons/DeleteButton/index.js @@ -1,5 +1,6 @@ import DeleteButton from './DeleteButton'; export default DeleteButton; +export { default as DeleteButtonRaw } from './DeleteButtonRaw'; export { default as DeletingButton } from './DeletingButton'; export { default as DeletedButton } from './DeletedButton'; export { default as DeletingFailedButton } from './DeletingFailedButton'; diff --git a/src/components/helpers/EnvironmentsList/EnvironmentsList.less b/src/components/helpers/EnvironmentsList/EnvironmentsList.less deleted file mode 100644 index fbe83b752..000000000 --- a/src/components/helpers/EnvironmentsList/EnvironmentsList.less +++ /dev/null @@ -1,3 +0,0 @@ -.environment { - margin: 2px; -} diff --git a/src/components/helpers/EnvironmentsList/EnvironmentsListItem.js b/src/components/helpers/EnvironmentsList/EnvironmentsListItem.js index 601a63746..f0638d6d1 100644 --- a/src/components/helpers/EnvironmentsList/EnvironmentsListItem.js +++ b/src/components/helpers/EnvironmentsList/EnvironmentsListItem.js @@ -1,11 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { OverlayTrigger, Tooltip, Label } from 'react-bootstrap'; -import styles from './EnvironmentsList.less'; const EnvironmentsListItem = ({ runtimeEnvironment, longNames = false }) => ( {runtimeEnvironment.description}}> - + ); diff --git a/src/components/icons/index.js b/src/components/icons/index.js index ad8b98c14..8a27605fb 100644 --- a/src/components/icons/index.js +++ b/src/components/icons/index.js @@ -32,6 +32,7 @@ export const GroupIcon = ({ organizational = false, archived = false, ...props } ); export const InfoIcon = props => ; +export const LimitsIcon = props => ; export const LoadingIcon = props => ; export const LocalIcon = props => ; export const LockIcon = props => ; @@ -63,6 +64,8 @@ export const SuccessOrFailureIcon = ({ success = false, ...props }) => export const SuperadminIcon = props => ; export const SupervisorIcon = props => ; export const SupervisorStudentIcon = props => ; +export const TagIcon = props => ; +export const TestsIcon = props => ; export const TransferIcon = props => ; const messageIconTypes = { diff --git a/src/containers/App/recodex.css b/src/containers/App/recodex.css index 8e25766da..0fde313a6 100644 --- a/src/containers/App/recodex.css +++ b/src/containers/App/recodex.css @@ -86,6 +86,11 @@ margin: 0 !important; } +.tag-margin { + margin: 2px; + display: inline-block; +} + /* * Padding Configurations */ diff --git a/src/containers/ExercisesListContainer/ExercisesListContainer.js b/src/containers/ExercisesListContainer/ExercisesListContainer.js index cda24af88..ea903212d 100644 --- a/src/containers/ExercisesListContainer/ExercisesListContainer.js +++ b/src/containers/ExercisesListContainer/ExercisesListContainer.js @@ -78,6 +78,9 @@ class ExercisesListContainer extends Component { + + + {showGroups && ( diff --git a/src/containers/ExercisesTagsEditContainer/ExercisesTagsEditContainer.js b/src/containers/ExercisesTagsEditContainer/ExercisesTagsEditContainer.js index 110e6b64c..fc7a24363 100644 --- a/src/containers/ExercisesTagsEditContainer/ExercisesTagsEditContainer.js +++ b/src/containers/ExercisesTagsEditContainer/ExercisesTagsEditContainer.js @@ -15,7 +15,7 @@ import { } from '../../redux/selectors/exercises'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import { getTagCSSColor } from '../../helpers/exercise/tags'; -import Icon, { LoadingIcon, RemoveIcon } from '../../components/icons'; +import { LoadingIcon, RemoveIcon, TagIcon } from '../../components/icons'; import Button from '../../components/widgets/FlatButton'; const ADD_TAG_INITIAL_VALUES = { tag: '' }; @@ -30,7 +30,7 @@ const ExercisesTagsEditContainer = ({ exercise, tags, tagsLoading, updatePending {exercise.tags.sort().map(tag => ( - + {tag} diff --git a/src/helpers/exercise/tags.js b/src/helpers/exercise/tags.js index 47bfeda75..ab72a5f01 100644 --- a/src/helpers/exercise/tags.js +++ b/src/helpers/exercise/tags.js @@ -8,6 +8,8 @@ export const getTagCSSColor = tag => { res = res + (char.charCodeAt(0) * goldenRatio) / 256; return Math.abs(res - Math.trunc(res)); }, 0); - const hue = Math.round(hash * 360); + const hue = Math.round(hash * 36) * 10; return `hsl(${hue}, 66%, 42%)`; }; + +export const getTagStyle = tag => ({ backgroundColor: getTagCSSColor(tag), color: 'white' }); diff --git a/src/locales/cs.json b/src/locales/cs.json index 9e5a2188a..a1bda37ae 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -545,7 +545,7 @@ "app.evaluationTable.score": "Skóre:", "app.exercise.assignButton": "Zadat", "app.exercise.assignToGroup": "Úloha může být zadána do více skupin, kde jste vedoucím. Taktéž je možné zadat úlohu individuálně v jednotlivých skupinách. Vezměte prosím na vědomí, že úloha může být zadána v jedné skupině vícekrát a tento formulář neposkytuje přehled již existujících zadání.", - "app.exercise.assignments": "Zadání", + "app.exercise.assignments": "Zadání ve skupinách", "app.exercise.attach": "Připojit", "app.exercise.breadcrumbTitle": "Úloha {name}", "app.exercise.description": "Krátký popis", @@ -577,9 +577,10 @@ "app.exerciseConfigTypeButton.advancedConfiguration": "Pokročilá konfigurace", "app.exerciseConfigTypeButton.confirm": "Tato operace nemusí být snadno vrácena zpět, protože změna typu konfigurace je možná jen za určitých podmínek. Přejete si pokračovat?", "app.exerciseConfigTypeButton.simpleConfiguration": "Jednoduchá konfigurace", - "app.exercises.difficultyIcon.easy": "Snadné", - "app.exercises.difficultyIcon.hard": "Obtížné", - "app.exercises.difficultyIcon.medium": "Průměrné", + "app.exercises.difficultyIcon.easy": "Snadná", + "app.exercises.difficultyIcon.hard": "Obtížná", + "app.exercises.difficultyIcon.medium": "Průměrná", + "app.exercises.difficultyIcon.unknown": "Neznámá", "app.exercises.failedDetail": "Načtení detailu úlohy se nezdařilo. Prosíme zkontrolujte své připojení k internetu a zkuste dotaz opakovat později.", "app.exercises.listEdit": "Nastavení", "app.exercises.listEditConfig": "Testy", @@ -1355,6 +1356,7 @@ "generic.submit": "Odevzdat", "generic.submitted": "Odevzdáno", "generic.submitting": "Odevzdávání...", + "generic.tags": "Nálepky", "generic.updated": "Změněno", "generic.uploadedAt": "Nahráno", "generic.validating": "Validování...", diff --git a/src/locales/en.json b/src/locales/en.json index 71784dac0..295c43410 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -545,7 +545,7 @@ "app.evaluationTable.score": "Score:", "app.exercise.assignButton": "Assign", "app.exercise.assignToGroup": "You can assign this exercise to multiple groups you supervise. The exercise can also be assigned from within the groups individually. Please note that an exercise may be assigned multiple times and this form does not track existing assignments.", - "app.exercise.assignments": "Assignments", + "app.exercise.assignments": "Assignments in Groups", "app.exercise.attach": "Attach", "app.exercise.breadcrumbTitle": "Exercise {name}", "app.exercise.description": "Short description", @@ -580,6 +580,7 @@ "app.exercises.difficultyIcon.easy": "Easy", "app.exercises.difficultyIcon.hard": "Hard", "app.exercises.difficultyIcon.medium": "Medium", + "app.exercises.difficultyIcon.unknown": "Unknown", "app.exercises.failedDetail": "Loading the details of the exercise failed. Please make sure you are connected to the Internet and try again later.", "app.exercises.listEdit": "Settings", "app.exercises.listEditConfig": "Tests", @@ -1355,6 +1356,7 @@ "generic.submit": "Submit", "generic.submitted": "Submitted", "generic.submitting": "Submitting...", + "generic.tags": "Tags", "generic.updated": "Updated", "generic.uploadedAt": "Uploaded at", "generic.validating": "Validating...", diff --git a/src/locales/whitelist_en.json b/src/locales/whitelist_en.json index 1e668fb64..bb9a0026c 100644 --- a/src/locales/whitelist_en.json +++ b/src/locales/whitelist_en.json @@ -580,6 +580,7 @@ "app.exercises.difficultyIcon.easy", "app.exercises.difficultyIcon.hard", "app.exercises.difficultyIcon.medium", + "app.exercises.difficultyIcon.unknown", "app.exercises.failedDetail", "app.exercises.listEdit", "app.exercises.listEditConfig", @@ -1355,6 +1356,7 @@ "generic.submit", "generic.submitted", "generic.submitting", + "generic.tags", "generic.updated", "generic.uploadedAt", "generic.validating",