-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Form for editing exercise tags created and placed on edit exercise page.
- Loading branch information
Martin Krulis
committed
Nov 25, 2019
1 parent
24f3068
commit f84167f
Showing
13 changed files
with
320 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
src/components/forms/AddExerciseTagForm/AddExerciseTagForm.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { FormattedMessage } from 'react-intl'; | ||
import { Form } from 'react-bootstrap'; | ||
import { reduxForm, Field } from 'redux-form'; | ||
|
||
import { TextField } from '../Fields'; | ||
import SubmitButton from '../SubmitButton'; | ||
import { AddIcon, LoadingIcon } from '../../../components/icons'; | ||
|
||
const AddExerciseTagForm = ({ | ||
submitting, | ||
handleSubmit, | ||
onSubmit, | ||
reset, | ||
submitFailed, | ||
submitSucceeded, | ||
invalid, | ||
tags, | ||
updatePending = false, | ||
}) => ( | ||
<Form> | ||
<table> | ||
<tbody> | ||
<tr> | ||
<td className="full-width valign-top"> | ||
<datalist id="knownExerciseTags"> | ||
{tags.map(tag => ( | ||
<option key={tag}>{tag}</option> | ||
))} | ||
</datalist> | ||
<Field | ||
name="tag" | ||
component={TextField} | ||
ignoreDirty | ||
groupClassName="full-width" | ||
list="knownExerciseTags" | ||
maxLength={16} | ||
/> | ||
</td> | ||
<td className="valign-top"> | ||
<SubmitButton | ||
id="addExerciseTag" | ||
disabled={invalid || updatePending} | ||
submitting={submitting} | ||
hasSucceeded={submitSucceeded} | ||
hasFailed={submitFailed} | ||
handleSubmit={handleSubmit(data => onSubmit(data).then(reset))} | ||
defaultIcon={updatePending ? <LoadingIcon gapRight /> : <AddIcon gapRight />} | ||
messages={{ | ||
submit: <FormattedMessage id="app.addExerciseTagForm.submit" defaultMessage="Add Tag" />, | ||
submitting: <FormattedMessage id="app.addExerciseTagForm.submitting" defaultMessage="Adding..." />, | ||
success: <FormattedMessage id="app.addExerciseTagForm.success" defaultMessage="Tag Added" />, | ||
}} | ||
/> | ||
</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</Form> | ||
); | ||
|
||
AddExerciseTagForm.propTypes = { | ||
exercise: PropTypes.object.isRequired, | ||
tags: PropTypes.array, | ||
updatePending: PropTypes.bool, | ||
submitting: PropTypes.bool, | ||
submitFailed: PropTypes.bool, | ||
submitSucceeded: PropTypes.bool, | ||
invalid: PropTypes.bool, | ||
handleSubmit: PropTypes.func.isRequired, | ||
onSubmit: PropTypes.func.isRequired, | ||
reset: PropTypes.func, | ||
}; | ||
|
||
const validate = ({ tag }, { exercise }) => { | ||
const errors = {}; | ||
if (!tag) { | ||
errors._error = true; | ||
return errors; | ||
} | ||
|
||
if (tag.length < 2) { | ||
errors.tag = ( | ||
<FormattedMessage id="app.addExerciseTagForm.validation.tooShort" defaultMessage="The tag name is too short." /> | ||
); | ||
return errors; | ||
} | ||
|
||
if (tag.length > 16) { | ||
errors.tag = ( | ||
<FormattedMessage id="app.addExerciseTagForm.validation.tooLong" defaultMessage="The tag name is too long." /> | ||
); | ||
return errors; | ||
} | ||
|
||
if (!tag.match(/^[-a-zA-Z0-9_]+$/)) { | ||
errors.tag = ( | ||
<FormattedMessage | ||
id="app.addExerciseTagForm.validation.invalidCharacters" | ||
defaultMessage="The tag name contains invalid characters. Only alphanumeric letters, dash and underscore are allowed." | ||
/> | ||
); | ||
return errors; | ||
} | ||
|
||
if (exercise && exercise.tags && exercise.tags.includes(tag)) { | ||
errors.tag = ( | ||
<FormattedMessage | ||
id="app.addExerciseTagForm.validation.alreadyAssigned" | ||
defaultMessage="Given tag is already assigned to the exercise." | ||
/> | ||
); | ||
return errors; | ||
} | ||
|
||
return errors; | ||
}; | ||
|
||
const warn = ({ tag }, { tags }) => { | ||
const warnings = {}; | ||
if (!tag) { | ||
return warnings; | ||
} | ||
|
||
if (tag.length < 3) { | ||
warnings.tag = ( | ||
<FormattedMessage id="app.addExerciseTagForm.warnings.tooShort" defaultMessage="The tag name is rather short." /> | ||
); | ||
return warnings; | ||
} | ||
|
||
if (tag.length > 12) { | ||
warnings.tag = ( | ||
<FormattedMessage id="app.addExerciseTagForm.warnings.tooLong" defaultMessage="The tag name is rather long." /> | ||
); | ||
return warnings; | ||
} | ||
|
||
if (tags && tags.length > 0) { | ||
if (!tags.includes(tag)) { | ||
warnings.tag = ( | ||
<FormattedMessage | ||
id="app.addExerciseTagForm.warnings.newTag" | ||
defaultMessage="You have specified a new tag, which has not been used in any exercise yet. Make sure that there is not a typo in your tag and there is no other tag with the same meaning but different key word." | ||
/> | ||
); | ||
} | ||
return warnings; | ||
} | ||
|
||
return warnings; | ||
}; | ||
|
||
export default reduxForm({ | ||
form: 'addExerciseTag', | ||
validate, | ||
warn, | ||
})(AddExerciseTagForm); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import AddExerciseTagForm from './AddExerciseTagForm'; | ||
export default AddExerciseTagForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
src/containers/ExercisesTagsEditContainer/ExercisesTagsEditContainer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import ImmutablePropTypes from 'react-immutable-proptypes'; | ||
import { connect } from 'react-redux'; | ||
import { FormattedMessage } from 'react-intl'; | ||
import { Table } from 'react-bootstrap'; | ||
|
||
import AddExerciseTagForm from '../../components/forms/AddExerciseTagForm'; | ||
import { addTag, removeTag } from '../../redux/modules/exercises'; | ||
import { | ||
exerciseSelector, | ||
getExerciseTags, | ||
getExerciseTagsLoading, | ||
getExerciseTagsUpdatePending, | ||
} from '../../redux/selectors/exercises'; | ||
import ResourceRenderer from '../../components/helpers/ResourceRenderer'; | ||
import { getTagCSSColor } from '../../helpers/exercise/tags'; | ||
import Icon, { LoadingIcon, RemoveIcon } from '../../components/icons'; | ||
import Button from '../../components/widgets/FlatButton'; | ||
|
||
const ADD_TAG_INITIAL_VALUES = { tag: '' }; | ||
|
||
const ExercisesTagsEditContainer = ({ exercise, tags, tagsLoading, updatePending, addTag, removeTag }) => ( | ||
<ResourceRenderer resource={exercise}> | ||
{exercise => ( | ||
<div> | ||
{exercise.tags && exercise.tags.length > 0 ? ( | ||
<Table hover condensed> | ||
<tbody> | ||
{exercise.tags.sort().map(tag => ( | ||
<tr key={`${exercise.id}:${tag}`}> | ||
<td> | ||
<Icon icon="tag" style={{ color: getTagCSSColor(tag) }}></Icon> | ||
</td> | ||
<td className="full-width">{tag}</td> | ||
<td> | ||
<Button bsStyle="danger" bsSize="xs" onClick={() => removeTag(tag)} disabled={updatePending}> | ||
{updatePending ? <LoadingIcon gapRight /> : <RemoveIcon gapRight />} | ||
<FormattedMessage id="generic.remove" defaultMessage="Remove" /> | ||
</Button> | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</Table> | ||
) : ( | ||
<p className="small text-muted text-center"> | ||
<FormattedMessage id="app.editExerciseTags.noTags" defaultMessage="no tags assigned" /> | ||
</p> | ||
)} | ||
<hr /> | ||
{tagsLoading ? ( | ||
<div>?</div> | ||
) : ( | ||
<React.Fragment> | ||
<AddExerciseTagForm | ||
exercise={exercise} | ||
tags={tags} | ||
onSubmit={data => addTag(data.tag)} | ||
initialValues={ADD_TAG_INITIAL_VALUES} | ||
updatePending={updatePending} | ||
/> | ||
</React.Fragment> | ||
)} | ||
</div> | ||
)} | ||
</ResourceRenderer> | ||
); | ||
|
||
ExercisesTagsEditContainer.propTypes = { | ||
exerciseId: PropTypes.string.isRequired, | ||
exercise: ImmutablePropTypes.map, | ||
tags: PropTypes.array, | ||
tagsLoading: PropTypes.bool.isRequired, | ||
updatePending: PropTypes.bool.isRequired, | ||
addTag: PropTypes.func.isRequired, | ||
removeTag: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default connect( | ||
(state, { exerciseId }) => ({ | ||
exercise: exerciseSelector(exerciseId)(state), | ||
tags: getExerciseTags(state), | ||
tagsLoading: getExerciseTagsLoading(state), | ||
updatePending: getExerciseTagsUpdatePending(state) !== null, | ||
}), | ||
(dispatch, { exerciseId }) => ({ | ||
addTag: tagName => dispatch(addTag(exerciseId, tagName)), | ||
removeTag: tagName => dispatch(removeTag(exerciseId, tagName)), | ||
}) | ||
)(ExercisesTagsEditContainer); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import ExercisesTagsEditContainer from './ExercisesTagsEditContainer'; | ||
export default ExercisesTagsEditContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const goldenRatio = (1 + Math.sqrt(5)) / 2; | ||
|
||
/** | ||
* A deterministic way to produce a random tag color from its name. | ||
*/ | ||
export const getTagCSSColor = tag => { | ||
const hash = tag.split('').reduce((res, char) => { | ||
res = res + (char.charCodeAt(0) * goldenRatio) / 256; | ||
return Math.abs(res - Math.trunc(res)); | ||
}, 0); | ||
const hue = Math.round(hash * 360); | ||
return `hsl(${hue}, 66%, 42%)`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.