Input validation for new annotations

This commit is contained in:
Alex Hunt 2018-11-23 18:18:42 -07:00
parent 49eb07102d
commit 0d74b9c189
12 changed files with 118 additions and 74 deletions

View File

@ -24,6 +24,11 @@ const editorStateActions = {
editorState: editorState,
command: command
}),
annotationCreated: (editorState) =>
({
type: 'ANNOTATION_CREATED',
editorState: editorState
}),
}
export default editorStateActions

View File

@ -114,7 +114,7 @@ export const EditorCancelButton = ({onClick}) =>
</button>
export const EditorSubmitButton = ({onClick}) =>
<button id='editor_submit' onClick={onClick} type='button' data-dismiss='modal' className='btn btn-outline-success btn-sm'>
<button id='editor_submit' onClick={onClick} type='button' className='btn btn-outline-success btn-sm'>
Submit
<i className='fa fa_inline fa-check-square-o'></i>
</button>

View File

@ -6,12 +6,15 @@ import TagColorPreview from './tagColorPreview'
import { DocumentList } from './list'
import helpers from '../modules/helpers'
const ChooseAnnotationModal = ({notes, tags, annotationNote, annotationTag, onSubmitClick, selectAnnotationNote, selectAnnotationTag, clearAnnotationTag}) =>
const ChooseAnnotationModal = ({notes, tags, annotationNote, annotationTag, onSubmitClick, selectAnnotationNote, selectAnnotationTag, clearAnnotationTag, userErrors}) =>
<div className='modal fade' id='annotate_modal' tabIndex='-1' role='dialog'>
<div className='modal-dialog modal-lg' role='document'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title' id='exampleModalLabel'>Select a note</h5>
<button id='select_annotation_modal_close' type="button" className="close" data-dismiss="modal">
<i className='fa fa-times'></i>
</button>
</div>
<div className='modal-body'>
<div className='row'>
@ -22,30 +25,37 @@ const ChooseAnnotationModal = ({notes, tags, annotationNote, annotationTag, onSu
<div dangerouslySetInnerHTML={{__html: annotationNote.html_source}} />
</div>
</div>
<div className='row'>
<div id='annotation_tag_dropdown' className='col-md-2'>
<button className={annotationNote.id ? 'btn btn-primary dropdown-toggle caret-off' : 'btn btn-primary dropdown-toggle caret-off disabled'} data-toggle='dropdown' type='button'><i className='fa fa-chevron-down'></i>Tags</button>
<div className='dropdown-menu'>
{tags.map(tag =>
<div key={tag.id} className='dropdown-item'>
<TagColorPreview color={tag.color}/>
<a className='select_annotation_tag' href='#' onClick={()=>selectAnnotationTag(tag)}>{tag.title}</a>
</div>
)}
</div>
<div className='row'>
<div id='annotation_tag_dropdown' className='col-md-2'>
<button className={annotationNote.id ? 'btn btn-primary dropdown-toggle caret-off' : 'btn btn-primary dropdown-toggle caret-off disabled'} data-toggle='dropdown' type='button'><i className='fa fa-chevron-down'></i>Tags</button>
<div className='dropdown-menu'>
{tags.map(tag =>
<div key={tag.id} className='dropdown-item' onClick={()=>selectAnnotationTag(tag)}>
<TagColorPreview color={tag.color}/>
<a className='select_annotation_tag'>{tag.title}</a>
</div>
)}
</div>
<div id='annotation_tag_preview' className='col-md-8 offset-md-1'>
{annotationTag.id &&
<div id='selected_annotation_tag'>
<TagColorPreview color={annotationTag.color}/>
{annotationTag.title}
<a id='clear_anntation_tag' href='#' onClick={clearAnnotationTag}>
<i className='fa fa-times'></i>
</a>
</div>
}
</div>
</div>
</div>
<div id='annotation_tag_preview' className='col-md-8 offset-md-1'>
{annotationTag.id &&
<div id='selected_annotation_tag'>
<TagColorPreview color={annotationTag.color}/>
{annotationTag.title}
<a id='clear_anntation_tag' onClick={clearAnnotationTag}>
<i className='fa fa-times'></i>
</a>
</div>
}
</div>
</div>
<div className='row'>
<div id='user_errors' className='col-md-12'>
{userErrors.map(error =>
<div key={error} className='user_error_message'>{error}</div>
)}
</div>
</div>
</div>
<div className='modal-footer'>
<EditorCancelButton />

View File

@ -28,6 +28,7 @@ const EditorPage = ({
selectAnnotationTag,
clearAnnotationTag,
selectionState,
userErrors,
}) =>
<div id='joyce_reader' className='container-fluid'>
<div className="row">
@ -55,6 +56,7 @@ const EditorPage = ({
selectAnnotationNote={selectAnnotationNote}
selectAnnotationTag={selectAnnotationTag}
clearAnnotationTag={clearAnnotationTag}
userErrors={userErrors}
/>
</div>
@ -69,6 +71,7 @@ const mapStateToProps = state => {
selectionState: state.selectionState,
docType: state.docType,
loadingToggle: state.loadingToggle,
userErrors: state.userErrors,
}
}

View File

@ -8,7 +8,7 @@ const Link = (props) => {
const data = props.contentState.getEntity(props.entityKey).getData()
return (
<a href='#'
onClick={()=>props.onAnnotationClick(data['url'])}
// onClick={()=>props.onAnnotationClick(data['url'])}
style={{color: '#' + data['data-color']}}
data-toggle='modal'
data-target='#annotation_modal'
@ -38,4 +38,4 @@ Link.propTypes = {
const LinkContainer = connect(mapStateToProps, mapDispatchToProps)(Link)
export default LinkContainer
export default Link

View File

@ -1,11 +1,11 @@
import axios from 'axios'
import { EditorState, Modifier } from 'draft-js'
import { stateToHTML } from 'draft-js-export-html'
import actions from '../actions'
import helpers from '../modules/helpers'
import { validateSubmittedDocument } from '../modules/validation'
import { html_export_options, convertToSearchText } from '../modules/editorSettings.js'
import { validateSubmittedDocument, validateSubmittedAnnotation } from '../modules/validation'
import { html_export_options, convertToSearchText, linkDecorator } from '../modules/editorSettings.js'
const joyceInterface = store => next => action => {
next(action)
@ -14,8 +14,8 @@ const joyceInterface = store => next => action => {
store.dispatch(actions.getDocumentText({id: action.id, docType: action.docType, state: 'currentDocument'}))
break
case 'SUBMIT_DOCUMENT_EDIT':
const errors = validateSubmittedDocument(action.docType, action.documentTitleInput, action.colorPickerInput)
if (errors.length < 1) {
const docErrors = validateSubmittedDocument(action.docType, action.documentTitleInput, action.colorPickerInput)
if (docErrors.length < 1) {
const textContent = action.editorState.getCurrentContent()
const data = { title: action.documentTitleInput, html_source: stateToHTML(textContent, html_export_options), search_text: convertToSearchText(textContent) }
if (action.docType === 'tags') {
@ -56,6 +56,28 @@ const joyceInterface = store => next => action => {
case 'SELECT_ANNOTATION_NOTE':
store.dispatch(actions.getDocumentText({id: action.id, docType: 'notes', state: 'annotationNote'}))
break
case 'SUBMIT_ANNOTATION':
const annotationErrors = validateSubmittedAnnotation(action.annotationNote, action.annotationTag)
if (annotationErrors.length < 1) {
const contentState = action.editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{'url': action.annotationNote.id, 'data-color': action.annotationTag.color, 'data-tag': action.annotationTag.title}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
action.selectionState,
entityKey
)
const newEditorState = EditorState.createWithContent(contentStateWithLink, linkDecorator)
store.dispatch(actions.annotationCreated(newEditorState))
// This feels like a hacky way of closing the modal only after validating input
// But seemed more idiomatic than writing jQuery
document.getElementById('select_annotation_modal_close').click()
}
break
// Search Action Middleware
case 'CLICK_SEARCH':
store.dispatch(actions.getSearchResults({data: action.data}))

View File

@ -1,4 +1,6 @@
import { convertToRaw } from 'draft-js'
import { convertToRaw, ContentState, CompositeDecorator } from 'draft-js'
import LinkContainer from '../containers/linkContainer'
export const html_export_options = {
blockStyleFn: (block) => {
@ -28,6 +30,25 @@ export const html_export_options = {
}
}
const findLinkEntities = (contentBlock, callback) => {
contentBlock.findEntityRanges(character => {
const contentState = ContentState.createFromBlockArray([contentBlock])
const entityKey = character.getEntity()
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
)
},
callback)
}
export const linkDecorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: LinkContainer,
}
])
export const convertToSearchText = contentState => {
const rawState = convertToRaw(contentState)
const searchText = rawState.blocks.reduce(

View File

@ -13,4 +13,15 @@ export const validateSubmittedDocument = (docType, documentTitleInput, colorPick
errors.push('Please enter a title.')
}
return errors
}
export const validateSubmittedAnnotation = (annotationNote, annotationTag) => {
const errors = []
if (!annotationNote.id) {
errors.push('Please choose a note.')
}
if (!annotationTag.id) {
errors.push('Please choose a tag.')
}
return errors
}

View File

@ -6,7 +6,7 @@ const annotationNote = (state={}, action) => {
} else { return state }
case 'ADD_ANNOTATION':
return {}
case 'SUBMIT_ANNOTATION':
case 'ANNOTATION_CREATED':
return {}
default:
return state

View File

@ -1,35 +1,16 @@
import React from 'react'
import { EditorState, Modifier, ContentState, CompositeDecorator, RichUtils, Entity } from 'draft-js'
import { EditorState, RichUtils } from 'draft-js'
import { stateFromHTML } from 'draft-js-import-html'
import LinkContainer from '../containers/linkContainer'
import { linkDecorator } from '../modules/editorSettings.js'
const blankEditor = EditorState.createEmpty(decorator)
const findLinkEntities = (contentBlock, callback) => {
contentBlock.findEntityRanges(character => {
const contentState = ContentState.createFromBlockArray([contentBlock])
const entityKey = character.getEntity()
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
)
},
callback)
}
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: LinkContainer,
}
])
const blankEditor = EditorState.createEmpty(linkDecorator)
const editorState = (state=blankEditor, action) => {
switch(action.type) {
case 'GET_DOCUMENT_TEXT':
if (action.status === 'success' && action.state === 'currentDocument') {
const editorState = EditorState.createWithContent(stateFromHTML(action.data.html_source), decorator)
const editorState = EditorState.createWithContent(stateFromHTML(action.data.html_source), linkDecorator)
return editorState
} else if (action.status === 'request' && action.state === 'currentDocument') {
return blankEditor
@ -53,27 +34,16 @@ const editorState = (state=blankEditor, action) => {
} else if (action.style === 'header-two') {
return RichUtils.toggleBlockType(action.editorState, 'header-two')
}
case 'SUBMIT_ANNOTATION':
const contentState = action.editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{'url': action.annotationNote.id, 'data-color': action.annotationTag.color, 'data-tag': action.annotationTag.title}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
action.selectionState,
entityKey
)
return EditorState.createWithContent(contentStateWithLink, decorator)
break
case 'ANNOTATION_CREATED':
return action.editorState
case 'REMOVE_ANNOTATION':
const contentStateWithoutLink = Modifier.applyEntity(
action.editorState.getCurrentContent(),
action.selectionState,
null
)
return EditorState.createWithContent(contentStateWithoutLink, decorator)
return EditorState.createWithContent(contentStateWithoutLink, linkDecorator)
default:
return state
}

View File

@ -2,7 +2,7 @@ const selectionState = (state={}, action) => {
switch(action.type) {
case 'ADD_ANNOTATION':
return action.data
case 'SUBMIT_ANNOTATION':
case 'ANNOTATION_CREATED':
return {}
default:
return state

View File

@ -1,9 +1,11 @@
import { validateSubmittedDocument } from '../modules/validation'
import { validateSubmittedDocument, validateSubmittedAnnotation } from '../modules/validation'
const userErrors = (state=[], action) => {
switch(action.type) {
case 'SUBMIT_DOCUMENT_EDIT':
return validateSubmittedDocument(action.docType, action.documentTitleInput, action.colorPickerInput)
case 'SUBMIT_ANNOTATION':
return validateSubmittedAnnotation(action.annotationNote, action.annotationTag)
case 'GET_DOCUMENT_TEXT':
if (action.status === 'success' && action.state === 'currentDocument') {
return []