Routing mostly working as expected now, except for switching docTypes on the edit page

This commit is contained in:
Alex Hunt 2018-05-08 19:54:40 -04:00
parent fde36c08d1
commit e1b4797c52
18 changed files with 259 additions and 154 deletions

View File

@ -32,11 +32,16 @@ REFACTORING CHECKLIST:
- Switch to Webpack Dev Server and Remove Webpack/Flask Dependencies
- Write ReadMe
- One Pass Through Every Module's Imports
- Rename Components
NEED:
- Deploy Process
- Testing
UX NEEDS:
- Disable Title Submit Button Without Title
- Disable Modal Submit Button with input
SHORT LIST:
- Make Annotate Mode Ignore Keyboard Input
- More Responsive CSS
@ -65,6 +70,7 @@ SHORT LIST:
- Advanced search page
- Filter by DocType, Chapter
- Drop Down window
- Handle no search results
- v0.5 Pagination
@ -80,7 +86,6 @@ SHORT LIST:
- Stats Logging
BUGS:
- DraftJS crashes if you backspace on an empty Editor
Short List:
- PropTypes

View File

@ -34,6 +34,11 @@
type: 'CANCEL_EDIT'
})
export const clearCurrentDocument = () =>
({
type: 'CLEAR_CURRENT_DOCUMENT'
})
// Click 'Delete' and confirm to delete a document
export const deleteCurrentDocument = (id, docType) =>
({

View File

@ -2,7 +2,6 @@ import React from 'react'
const SearchResultSnippet = ({snippet}) =>
<div>
{console.log(snippet)}
{snippet}...
</div>

View File

@ -5,6 +5,7 @@ import { DocumentList } from '../components/list'
import { NewDocumentButton } from '../components/button'
import { DocTypeDropdown } from '../components/dropdown'
import SidebarSpacer from '../components/sidebarSpacer'
import { push } from 'react-router-redux'
const JoyceEditorSidebar = ({notes, chapters, docType, currentDocument, onDocumentClick, onNewDocumentClick, setDocType}) =>
<div className="col-md-3 d-none d-md-block" id="sidebar">

View File

@ -4,6 +4,7 @@ import { setCurrentDocument, toggleHighlight } from '../actions/userActions'
import { DocumentList } from '../components/list'
import { HighlightButton } from '../components/button'
import SidebarSpacer from '../components/sidebarSpacer'
import { push } from 'react-router-redux'
const JoyceReaderSidebar = ({chapters, notes, currentDocument, onDocumentClick, highlightToggle, onHighlightClick, docType}) =>
<div className='col-md-3 d-none d-md-block' id='sidebar'>

View File

@ -16,8 +16,8 @@ import { getDocumentList } from './actions/apiActions'
import DeleteConfirmModal from './components/deleteConfirmModal'
import AnnotateModal from './components/annotateModal'
import AnnotationModal from './components/annotationModal'
import { setCurrentDocument, setDocType } from './actions/userActions'
import { logger, joyceAPI } from './middleware/'
import { setDocType } from './actions/userActions'
import { logger, joyceAPI, joyceInterface, joyceRouter } from './middleware/'
import JoyceReaderPageContainer from './containers/joyceReaderPageContainer'
import JoyceEditorPageContainer from './containers/joyceEditorPageContainer'
import JoyceSearchPageContainer from './containers/joyceSearchPageContainer'
@ -26,9 +26,8 @@ import JoyceSearchPageContainer from './containers/joyceSearchPageContainer'
const history = createHistory()
const router = routerMiddleware(history)
const store = createStore(reduceReader, applyMiddleware(logger, joyceAPI, router))
const store = createStore(reduceReader, applyMiddleware(logger, router, joyceAPI, joyceInterface, joyceRouter))
store.dispatch(setDocType('chapters'))
store.dispatch(getDocumentList({docType: 'chapters'}))
store.dispatch(getDocumentList({docType: 'notes'}))
@ -44,14 +43,15 @@ ReactDOM.render(
<Route exact path='/edit' render={() =>
<Redirect to={'/edit/:id'}/>
}/>
<Route exact path='/edit/notes' render={() =>
<Redirect to={'/edit/notes/:id'}/>
}/>
<Route exact path='/notes' render={() =>
<Redirect to={'/notes/:id'}/>
}/>
<Route exact path='/' render={() =>
<Redirect to={'/:id'}/>
}/>
}/>
<Route exact path='/notes/:id' component={JoyceReaderPageContainer} />
<Route exact path='/edit/:id' component={JoyceEditorPageContainer} />
<Route exact path='/edit/notes/:id' component={JoyceEditorPageContainer} />
<Route exact path='/search/' component={JoyceSearchPageContainer} />
<Route exact path='/:id' component={JoyceReaderPageContainer} />
</Switch>

View File

@ -1,48 +0,0 @@
import axios from 'axios'
const apiRoute = '/api/'
// Axios HTTP Functions
export const HTTPGetDocumentList = (docType, state) =>
axios.get(apiRoute + docType).then(res => {
return {status: 'success', docType: docType, state: state, data: res.data}
}).catch(error => {
return {status: 'error', docType: docType, state: state, data: error}
})
export const HTTPGetDocumentText = (id, docType, state) =>
axios.get(apiRoute + docType + '/' + id).then(res => {
return {id: id, status: 'success', docType: docType, state: state, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, state: state, data: error}
})
export const HTTPDeleteDocument = (id, docType) =>
axios.delete(apiRoute + docType + '/' + id).then(res => {
return {id: id, status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, data: error}
})
export const HTTPPutCreateDocument = (docType, data) =>
axios.put(apiRoute + docType + '/', data).then(res => {
return {status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {status: 'error', docType: docType, data: error}
})
export const HTTPPostWriteDocument = (id, docType, data) =>
axios.post(apiRoute + docType + '/' + id, data).then(res => {
return {id: data.id, status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, data: error}
})
export const HTTPPostSearchResults = (data) =>
axios.post(apiRoute + 'search/', { data }).then(res => {
console.log('data is ', data)
return {status: 'success', data: res.data}
}).catch(error => {
return {status: 'error', data: res.data}
})

View File

@ -1,7 +1,11 @@
import { joyceAPI } from './joyceAPI'
import { logger } from './logger'
import joyceAPI from './joyceAPI'
import joyceInterface from './joyceInterface'
import joyceRouter from './joyceRouter'
import logger from './logger'
export {
joyceAPI,
joyceInterface,
joyceRouter,
logger
}

View File

@ -1,4 +1,3 @@
import axios from 'axios'
import { push } from 'react-router-redux'
import {
@ -10,42 +9,23 @@ import {
getSearchResults
} from '../actions/apiActions'
import { setCurrentDocument } from '../actions/userActions'
import {
HTTPGetDocumentList,
HTTPGetDocumentText,
HTTPDeleteDocument,
HTTPPutCreateDocument,
HTTPPostWriteDocument,
HTTPPostSearchResults } from './http'
import helpers from '../modules/helpers'
import api from '../modules/api'
import regex from '../modules/regex'
// API Middleware
export const joyceAPI = store => next => action => {
const joyceAPI = store => next => action => {
next(action)
switch(action.type) {
// API Calls
case 'GET_DOCUMENT_LIST':
if (action.status === 'request') {
HTTPGetDocumentList(action.docType, action.state).then(response =>
api.HTTPGetDocumentList(action.docType, action.state).then(response =>
store.dispatch(getDocumentList(response))
)
}
if (action.status === 'success' && action.docType === store.getState().docType && !store.getState().currentDocument.id) {
if (helpers.checkIfRedirectPath(store.getState().routerReducer.location.pathname)) {
if (action.docType === 'chapters') {
store.dispatch(push(action.data[0].number))
} else {
store.dispatch(push(action.data[0].id))
}
}
}
break
case 'GET_DOCUMENT_TEXT':
if (action.status === 'request') {
HTTPGetDocumentText(action.id, action.docType, action.state).then(response =>
api.HTTPGetDocumentText(action.id, action.docType, action.state).then(response =>
store.dispatch(getDocumentText(response))
)
}
@ -53,28 +33,33 @@ export const joyceAPI = store => next => action => {
case 'SAVE_DOCUMENT':
if (action.status === 'request') {
if (action.id) {
HTTPPostWriteDocument(action.id, action.docType, action.data).then(response =>
store.dispatch(saveDocument(response))
api.HTTPPostWriteDocument(action.id, action.docType, action.data).then(response =>
store.dispatch(saveDocument(response)
)
)} else {
HTTPPutCreateDocument(action.docType, action.data).then(response =>
store.dispatch(saveDocument(response)))
api.HTTPPutCreateDocument(action.docType, action.data).then(response =>
store.dispatch(saveDocument(response))
)
}
} else if (action.status === 'success' && !action.id) {
store.dispatch(setCurrentDocument(action.data.slice(-1)[0].id, action.docType))
}
break
case 'DELETE_DOCUMENT':
if (action.status === 'request') {
HTTPDeleteDocument(action.id, action.docType).then(response =>
api.HTTPDeleteDocument(action.id, action.docType).then(response =>
store.dispatch(deleteDocument(response))
)
} else if (action.status === 'success') {
if (action.data[0]) {
store.dispatch(setCurrentDocument(action.data[0].id, action.docType, 'currentDocument'))
}
}
break
case 'GET_SEARCH_RESULTS':
if (action.status === 'request') {
api.HTTPPostSearchResults(action.data).then(response =>
store.dispatch(getSearchResults(response))
)
}
break
default:
break
}
}
}
export default joyceAPI

View File

@ -3,6 +3,17 @@ import { stateToHTML } from 'draft-js-export-html'
import { stateToMarkdown } from 'draft-js-export-markdown'
import { convertToRaw } from 'draft-js'
import {
getDocumentList,
getDocumentText,
deleteDocument,
saveDocument,
createNewChapter,
getSearchResults
} from '../actions/apiActions'
import { clearCurrentDocument } from '../actions/userActions'
import helpers from '../modules/helpers'
const html_export_options = {
@ -22,8 +33,7 @@ const html_export_options = {
}
}
// API Middleware
export const joyceInterface = store => next => action => {
const joyceInterface = store => next => action => {
next(action)
switch(action.type) {
case 'SET_CURRENT_DOCUMENT':
@ -57,6 +67,11 @@ export const joyceInterface = store => next => action => {
store.dispatch(getDocumentText({id: notes[0].id, docType: docType, state: 'currentDocument'}))
}
break
case 'SET_DOC_TYPE':
if (action.docType !== store.getState().docType) {
store.dispatch(clearCurrentDocument())
}
break
// Annotation Action Middleware
case 'SELECT_ANNOTATION_NOTE':
store.dispatch(getDocumentText({id: action.id, docType: 'notes', state: 'annotationNote'}))
@ -65,14 +80,9 @@ export const joyceInterface = store => next => action => {
case 'CLICK_SEARCH':
store.dispatch(getSearchResults({data: action.data}))
break
case 'GET_SEARCH_RESULTS':
if (action.status === 'request') {
HTTPPostSearchResults(action.data).then(response =>
store.dispatch(getSearchResults(response))
)
}
break
default:
break
}
}
}
export default joyceInterface

View File

@ -1,27 +1,80 @@
export const joyceRouter = store => next => action => {
import { push } from 'react-router-redux'
import { setCurrentDocument, setDocType } from '../actions/userActions'
import helpers from '../modules/helpers'
import regex from '../modules/regex'
const joyceRouter = store => next => action => {
next(action)
const path = store.getState().routerReducer.location !== null ? store.getState().routerReducer.location.pathname : '/'
const pathID = regex.checkPathForID(path) ? regex.parseIDFromPath(path) : undefined
const pathNumber = regex.checkPathForNumber(path) ? regex.parseNumberFromPath(path) : undefined
const docType = store.getState().docType
const currentDocument = store.getState().currentDocument
const chapters = store.getState().chapters
const notes = store.getState().notes
switch(action.type) {
case '@@router/LOCATION_CHANGE':
const path = action.payload.pathname
const docType = store.getState().docType
const currentDocument = store.getState().currentDocument
switch(path) {
case /^\/[0-9]{,3}/.test(path):
// set docType = 'chapters'
// set Current Chapter
case /^\/notes\/[a-zA-Z\-\_]{18,}/.test(path):
// set docType = 'notes'
// set current note
case /^\/edit\/notes\/[a-zA-Z\-\_]{18,}]/.test(path):
// set docType = 'chapters'
// set Current Chapter
// case: if /edit/ and docType = notes => /edit/notes/
// case: if redirectURL
if (regex.checkIfRedirectPath(path) && currentDocument.hasOwnProperty('id')) {
store.dispatch(push(docType === 'chapters' ? String(currentDocument.number) : currentDocument.id))
}
if (regex.checkNoteReaderRoute(path) && regex.checkIfRedirectPath(path) && notes.length > 0) {
store.dispatch(setCurrentDocument(notes[0].id, 'notes'))
}
if (regex.checkNoteReaderRoute(path) || regex.checkNoteEditorRoute(path)) {
store.dispatch(setDocType('notes'))
}
if (regex.checkRootRedirectRoute(path) && chapters.length > 0) {
store.dispatch(setCurrentDocument(chapters[0].id, 'chapters'))
}
if (regex.checkNoteBaseRoute(path)) {
store.dispatch(setDocType('notes'))
}
if (regex.checkEditBaseRoute(path)){
if (docType === 'notes') {
store.dispatch(push('/edit/notes'))
}
}
case 'GET_DOCUMENT_LIST':
if (action.status === 'success' && action.docType === docType && !currentDocument.id) {
if (regex.checkIfRedirectPath(path)) {
store.dispatch(setCurrentDocument(action.data[0].id, action.docType))
}
if (action.docType === 'chapters' && pathNumber !== undefined) {
for (const chapter of action.data) {
if (chapter.number === pathNumber) {
store.dispatch(setCurrentDocument(chapter.id, action.docType))
}
}
} else if (pathID !== undefined) {
store.dispatch(setCurrentDocument(pathID, action.docType))
}
}
break
case 'GET_DOCUMENT_TEXT':
if (action.status === 'success') {
store.dispatch(push(action.docType === 'chapters' ? String(action.data.number) : action.data.id))
}
break
case 'SAVE_DOCUMENT':
if (action.status === 'success' && !action.id) {
store.dispatch(setCurrentDocument(action.data.slice(-1)[0].id, action.docType))
}
break
case 'DELETE_DOCUMENT':
if (action.status === 'request') {
api.HTTPDeleteDocument(action.id, action.docType).then(response =>
store.dispatch(deleteDocument(response))
)
} else if (action.status === 'success') {
if (action.data[0]) {
store.dispatch(setCurrentDocument(action.data[0].id, action.docType, 'currentDocument'))
}
}
break
default:
break
}
}
export default joyceRouter

View File

@ -1,8 +1,10 @@
export const logger = store => next => action => {
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
}
export default logger

44
src/modules/api.js Normal file
View File

@ -0,0 +1,44 @@
import axios from 'axios'
const apiRoute = '/api/'
const api = {
HTTPGetDocumentList: (docType, state) =>
axios.get(apiRoute + docType).then(res => {
return {status: 'success', docType: docType, state: state, data: res.data}
}).catch(error => {
return {status: 'error', docType: docType, state: state, data: error}
}),
HTTPGetDocumentText: (id, docType, state) =>
axios.get(apiRoute + docType + '/' + id).then(res => {
return {id: id, status: 'success', docType: docType, state: state, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, state: state, data: error}
}),
HTTPDeleteDocument: (id, docType) =>
axios.delete(apiRoute + docType + '/' + id).then(res => {
return {id: id, status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, data: error}
}),
HTTPPutCreateDocument: (docType, data) =>
axios.put(apiRoute + docType + '/', data).then(res => {
return {status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {status: 'error', docType: docType, data: error}
}),
HTTPPostWriteDocument: (id, docType, data) =>
axios.post(apiRoute + docType + '/' + id, data).then(res => {
return {id: data.id, status: 'success', docType: docType, data: res.data}
}).catch(error => {
return {id: id, status: 'error', docType: docType, data: error}
}),
HTTPPostSearchResults: (data) =>
axios.post(apiRoute + 'search/', { data }).then(res => {
console.log('data is ', data)
return {status: 'success', data: res.data}
}).catch(error => {
return {status: 'error', data: res.data}
})
}
export default api

View File

@ -1,26 +1,12 @@
import regex from '../modules/regex'
const helpers = {
checkIfRedirectPath: path => {
const match = /\/(\:id)$/.exec(path)
if (match) {
return true
} else {
return false
}
},
parseNumberFromPath: path => {
const match = /\/([0-9]*)$/.exec(path)
if (match && parseInt(match[1])) {
return Number(match[1])
} else {
return null
}
},
parseIDFromPath: path => {
const match = /\/([A-Za-z0-9\-\_]{18,})$/.exec(path)
if (match) {
return match[1]
} else {
return null
getChapterIDFromPath: (path, chapters) => {
for (const chapter of chapters) {
const number = regex.parseNumberFromPath(path)
if (chapter.number == number) {
return chapter.id
}
}
}
}

View File

@ -0,0 +1,55 @@
const regexCheckBaseFunction = (path, pattern) => {
const match = pattern.exec(path)
if (match) {
return true
} else {
return false
}
}
const regexParseBaseFunction = (path, pattern) => {
const match = pattern.exec(path)
if (match) {
return match[1]
} else {
return null
}
}
const regex = {
checkIfRedirectPath: path => {
return regexCheckBaseFunction(path, /\/(\:id)$/)
},
checkNoteBaseRoute: path => {
return regexCheckBaseFunction(path, /^\/notes$/)
},
checkEditBaseRoute: path => {
return regexCheckBaseFunction(path, /^\/edit$/)
},
checkChapterReaderRoute: path => {
return regexCheckBaseFunction(path, /^\/[0-9]{1,3}$/)
},
checkNoteReaderRoute: path => {
return regexCheckBaseFunction(path, /^\/notes\/([0-9a-zA-Z\-\_]{18,}|\:id)/)
},
checkNoteEditorRoute: path => {
return regexCheckBaseFunction(path, /^\/edit\/notes\/([0-9a-zA-Z\-\_]{18,}|\:id)/)
},
checkPathForNumber: path => {
return regexCheckBaseFunction(path, /\/[0-9]{1,3}$/)
},
checkRootRedirectRoute: path => {
return regexCheckBaseFunction(path, /^\/\:id$/)
},
checkPathForID: path => {
return regexCheckBaseFunction(path, /\/([0-9A-Za-z0-9\-\_]{18,})$/)
},
parseNumberFromPath: path => {
return Number(regexParseBaseFunction(path, /\/([0-9]{1,3})$/))
},
parseIDFromPath: path => {
return regexParseBaseFunction(path, /\/([0-9A-Za-z0-9\-\_]{18,})$/)
}
}
export default regex

View File

@ -7,6 +7,8 @@ const currentDocument = (state={}, action) => {
return {}
}
else { return state }
case 'CLEAR_CURRENT_DOCUMENT':
return {}
case 'DELETE_DOCUMENT':
if (action.status === 'success' && action.data.length <= 0) {
return {}

View File

@ -2,11 +2,8 @@ const docType = (state='chapters', action) => {
switch(action.type) {
case 'SET_DOC_TYPE':
return action.docType
case '@@router/LOCATION_CHANGE':
const path = action.payload.pathname
if (/^\/(notes).*/.exec(path)) {
return 'notes'
} else{ return state }
case 'SET_CURRENT_DOCUMENT':
return action.docType
default:
return state
}

View File

@ -41,7 +41,11 @@ const editorState = (state=blankEditor, action) => {
case 'UPDATE_EDITOR_STATE':
return action.data
case 'HANDLE_EDITOR_KEY_COMMAND':
return RichUtils.handleKeyCommand(action.editorState, action.command)
const editorState = RichUtils.handleKeyCommand(action.editorState, action.command)
// Null check to handle null editorState when backspacking empty editor
if (editorState !== null) {
return editorState
} else { return state }
case 'APPLY_INLINE_STYLE':
let inlineStyles = ['BOLD', 'ITALIC', 'UNDERLINE']
if (inlineStyles.indexOf(action.style) >= 0) {