Input validation for new annotations
This commit is contained in:
parent
49eb07102d
commit
0d74b9c189
|
@ -24,6 +24,11 @@ const editorStateActions = {
|
|||
editorState: editorState,
|
||||
command: command
|
||||
}),
|
||||
annotationCreated: (editorState) =>
|
||||
({
|
||||
type: 'ANNOTATION_CREATED',
|
||||
editorState: editorState
|
||||
}),
|
||||
}
|
||||
|
||||
export default editorStateActions
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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}))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 []
|
||||
|
|
Loading…
Reference in New Issue