add server rendering of loaded compares and clean up saving of compares, switch to using isomorphic fetch (and save much compiled size)

This commit is contained in:
Adam Brown 2016-12-14 19:06:14 -05:00
parent 0a3a37e64e
commit 6bb76ccd17
17 changed files with 2456 additions and 19660 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@ data/*
browser-bundle.js.map
npm-debug.log.*
stats.json
stats.analyzed.txt

21781
dist/browser-bundle.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -4,7 +4,7 @@
"description": "",
"main": "src/server/babel.index.js",
"scripts": {
"copy-css": "copyfiles -f ./node_modules/semantic-ui-css/semantic.min.css ./dist",
"copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist",
"build": "npm run copy-css && webpack --colors",
"start": "npm run copy-css && webpack --progress --colors --watch",
"serve": "node src/server/babel.index.js",
@ -20,15 +20,15 @@
"body-parser": "^1.15.2",
"diff": "^3.0.1",
"express": "^4.14.0",
"isomorphic-fetch": "^2.2.1",
"jsonfile": "^2.4.0",
"markdown-it": "^5.1.0",
"markdown-to-jsx": "^4.0.3",
"react": "^0.14.5",
"react-dom": "^0.14.5",
"react-redux": "^4.4.6",
"react-router": "^1.0.0",
"react-router": "~3.0.0",
"redux": "^3.5.1",
"redux-router": "^1.0.0-beta5",
"redux-thunk": "^2.1.0",
"request": "^2.79.0",
"request-promise-native": "^1.0.3",

View File

@ -5,8 +5,8 @@ import * as Redux from 'redux'
import {Provider} from 'react-redux'
import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, Route, IndexRoute, Redirect } from 'react-router'
//import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, Route, IndexRoute, Redirect, browserHistory } from 'react-router'
import thunk from 'redux-thunk'
@ -38,8 +38,11 @@ const store = Redux.createStore(
Redux.applyMiddleware(...middlewares)
)
console.log(store)
//this way of reading local input isn't working:
// it's just overriding what comes from the server
// and it's not respecting the comparison that is loaded from the server
/*
const localInput = localStore.get('dubdiff')
if (localInput.input) {
//dispatch localStore data to store
@ -47,6 +50,7 @@ if (localInput.input) {
store.dispatch(Actions.updateFinalInput(localInput.input.final))
//should this be done after the first render?
}
*/
//save the state whenever the state changes
function saveState() {
@ -60,7 +64,7 @@ store.subscribe(saveState)
function render() {
ReactDOM.render(
<Provider store={store}>
<Router history={createBrowserHistory()}>
<Router history={browserHistory}>
{routes}
</Router>
</Provider>

View File

@ -1,4 +1,4 @@
import requestPromise from 'request-promise-native'
import fetch from 'isomorphic-fetch'
import uuid from 'uuid/v4'
export const updateOriginalInput = (text) =>
@ -24,43 +24,82 @@ export const clearInput = () =>
dispatch({ type: 'SAVE_STATUS_EMPTY'})
}
export const updateOriginalCompare = (text) => ({ type: 'UPDATE_ORIGINAL_COMPARE', data:text})
export const updateFinalCompare = (text) => ({ type: 'UPDATE_FINAL_COMPARE', data:text})
export const clearCompare = () => ({ type: 'CLEAR_COMPARE'})
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT'})
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT'})
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL'})
export const showFinal = () => ({ type: 'SHOW_FINAL'})
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE'})
//saves the current input fields to the server
//creates and returns a new id for the
export const save = () =>
(dispatch, getState) => {
console.log("!!! SAVING")
//generate an id
const id = uuid()
dispatch( {type: 'SAVE_STATUS_ASSIGN_ID', id})
//set waiting state
dispatch( {type: 'SAVE_STATUS_WAITING'})
const reqOptions = {
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'POST',
uri: `${location.origin}/api/compare/${id}`,
body: {
body: JSON.stringify({
a: getState().input.original,
b: getState().input.final
}),
headers: {
"Content-Type": "application/json"
},
json: true
}
//dispatch post request
requestPromise(reqOptions)
.then(returnBodyJson => {
fetch(endpointUri, fetchOptions)
.then(response => {
dispatch( {type: 'SAVE_STATUS_SAVED'})
})
.catch(error => {
dispatch( {type: 'SAVE_STATUS_FAILED', error})
})
//return the id after the request has been sent
return id;
}
/*
const load = (id) =>
(dispatch, getState) => {
//set waiting state
dispatch( {type: 'SAVE_STATUS_WAITING'})
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'GET'
}
//dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => response.json())
.then(json => {
dispatch( {type: 'UPDATE_ORIGINAL_INPUT', data:json.a})
dispatch( {type: 'UPDATE_FINAL_INPUT', data:json.b})
dispatch( {type: 'LOAD_STATUS_LOADED'})
})
.catch(error => {
dispatch( {type: 'LOAD_STATUS_FAILED', error})
})
//return the id after the request has been sent
return id;
}
export const loadIfNeeded = (id) =>
(dispatch, getState) => {
if
}
*/

View File

@ -18,19 +18,24 @@ const mapStateToProps = (state) => ({
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state),
compare: state.compare,
safeInput: Selectors.safeInput(state),
diff: Selectors.diff(state)
})
const mapDispatchToProps = dispatch => ({
//loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
})
class Compare extends React.Component {
/*
componentDidMount() {
this.props.loadIfNeeded(this.props.routeParams.compareId)
}
*/
render() {
console.log({isMarkdownFormat: this.props.isMarkdownFormat, isShowDifference: this.props.isShowDifference})
return (
<div>
<Header/>
@ -49,11 +54,11 @@ class Compare extends React.Component {
<ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>:
(!this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowPlaintext
text={this.props.isShowOriginal? this.props.compare.original: this.props.compare.final}
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> :
(this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowMarkdown
text={this.props.isShowOriginal? this.props.compare.original: this.props.compare.final}
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> :
null
}

View File

@ -12,7 +12,6 @@ const mapStateToProps = (state) => ({
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state),
compare: state.compare
})
@ -22,17 +21,14 @@ const mapDispatchToProps = dispatch => ({
onShowOriginal: () => dispatch(Actions.showOriginal()),
onShowFinal: () => dispatch(Actions.showFinal()),
onShowDifference: () => dispatch(Actions.showDifference()),
onEdit: (compare) => {
dispatch(Actions.updateOriginalInput(compare.original))
dispatch(Actions.updateFinalInput(compare.final))
dispatch(Actions.clearCompare())
onEdit: () => {
}
})
class CompareControls extends React.Component {
onClickEdit() {
this.props.onEdit(this.props.compare)
this.props.onEdit()
}
onClickMarkdownFormat() {

View File

@ -1,10 +1,11 @@
import React from 'react'
import {connect} from 'react-redux'
import {Segment, Header} from 'semantic-ui-react'
import {Segment, Header, Rail, Container} from 'semantic-ui-react'
import {Link} from 'react-router'
import * as Actions from '../actions'
import SaveStatus from './SaveStatus'
const mapStateToProps = (state) => ({
})
@ -18,9 +19,21 @@ const mapDispatchToProps = dispatch => ({
})
const SiteHeader = (props) => (
<Segment basic padded textAlign="center" as="header" id='masthead'>
<Link to="/"><Header onClick={props.onClear}>dubdiff</Header></Link>
</Segment>
<Segment basic >
<Segment basic padded textAlign="center" as="header" id='masthead'>
<Header><Link onClick={props.onClear} to="/">dubdiff</Link></Header>
</Segment>
<Rail internal position="right">
<Segment basic padded>
<SaveStatus/>
</Segment>
</Rail>
</Segment>
)
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)

View File

@ -1,6 +1,6 @@
import React from 'react'
import {connect} from 'react-redux'
import {Link} from 'react-router'
import {browserHistory} from 'react-router'
import {Button, Icon, Segment} from 'semantic-ui-react'
@ -10,27 +10,30 @@ import * as Selectors from '../selectors'
const mapStateToProps = (state) => ({
format: state.format,
isMarkdownFormat: Selectors.isMarkdownFormat(state),
safeInput: Selectors.safeInput(state)
saveStatus: state.saveStatus
})
const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
onCompare: (safeInput) => {
dispatch(Actions.save())
dispatch(Actions.updateOriginalCompare(safeInput.original))
dispatch(Actions.updateFinalCompare(safeInput.final))
//returns an id for the record to be saved
startSaveAsync: () => {
return dispatch(Actions.save())
}
})
class MainControls extends React.Component {
onClickCompare() {
//generate new id? (or should the id be baked into the link route?)
//post safeInput to db
//start saving the input to the server
const id = this.props.startSaveAsync()
//we can use the id created by the save method to build a path
const comparePath = `/${id}`
browserHistory.replace(comparePath)
this.props.onCompare(this.props.safeInput)
return false
}
@ -46,7 +49,7 @@ class MainControls extends React.Component {
return (
<Segment.Group>
<Segment >
<Link to="compare"><Button fluid onClick={this.onClickCompare.bind(this)}>Compare</Button></Link>
<Button fluid onClick={this.onClickCompare.bind(this)}>Compare</Button>
</Segment>
<Segment >

View File

@ -0,0 +1,52 @@
import React from 'react'
import {connect} from 'react-redux'
import { Message, Icon, Button} from 'semantic-ui-react'
import { browserHistory} from 'react-router'
import * as Actions from '../actions'
const mapStateToProps = (state) => ({
saveStatus: state.saveStatus
})
const mapDispatchToProps = dispatch => ({
retrySave: () => dispatch(Actions.save())
})
const onRetrySaveClick = (props) => {
//we can use the id created by the save method to build a path
const id = props.retrySave()
const comparePath = `/${id}`
browserHistory.replace(comparePath)
return false
}
const SaveStatus = (props) => {
if (props.saveStatus.waiting) return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Saving diff</Message.Header>
</Message.Content>
</Message>
)
else if (props.saveStatus.failed) return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error saving diff</Message.Header>
The server returned {props.saveStatus.error.message}.
<Button onClick={()=>onRetrySaveClick(props)}>Retry</Button>
</Message.Content>
</Message>
)
else return ( <div></div> )
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)

10
src/common/constants.js Normal file
View File

@ -0,0 +1,10 @@
export const Format = {
PLAINTEXT: 'PLAINTEXT',
MARKDOWN: 'MARKDOWN'
}
export const Show = {
ORIGINAL:'ORIGINAL',
FINAL:'FINAL',
DIFFERENCE:'DIFFERENCE'
}

View File

@ -14,20 +14,6 @@ export function input (state, action ) {
}
}
export function compare (state, action ) {
switch (action.type) {
case 'UPDATE_ORIGINAL_COMPARE':
return Object.assign({}, state, {original:action.data})
case 'UPDATE_FINAL_COMPARE':
return Object.assign({}, state, {final:action.data})
case 'CLEAR_COMPARE':
return {original:'', final:''}
default:
return state || {original:'', final:''}
}
}
export function format (state, action) {
switch (action.type) {
case 'SET_PLAINTEXT_FORMAT':
@ -57,18 +43,30 @@ export function show (state, action) {
export function saveStatus (state, action) {
switch (action.type) {
case 'SAVE_STATUS_DIRTY':
return {dirty:true, id:null}
return {dirty: true}
case 'SAVE_STATUS_EMPTY':
return {dirty:false, id:null}
return {dirty: false, empty: true}
case 'SAVE_STATUS_SAVED':
return Object.assign({}, state, {waiting: false, dirty:false, failed: false, error:null})
return {dirty: false, saved: true}
case 'SAVE_STATUS_FAILED' :
return Object.assign({}, state, {waiting: false, failed: true, error: action.error})
case 'SAVE_STATUS_WAITING' :
return Object.assign({}, state, {waiting: true, failed: false, error: null })
case 'SAVE_STATUS_ASSIGN_ID':
return Object.assign({}, state, {id: action.id})
return Object.assign({}, state, {waiting: true, failed: false, error: null})
default:
return state || {empty: true, dirty:false, id:null}
return state || {empty: true, dirty:false}
}
}
/*
export function loadStatus (state, action) {
switch (action.type) {
case 'LOAD_STATUS_WAITING':
return {waiting: true}
case 'LOAD_STATUS_FAILED':
return {failed: true, error: action.error }
case 'LOAD_STATUS_LOADED':
return {loaded: true}
default:
return state || {waiting: false}
}
}
*/

View File

@ -13,7 +13,6 @@ import * as Dubdiff from './util/dubdiff'
const input = (state) => state.input
const format = (state) => state.format
const show = (state) => state.show
const compare = (state) => state.compare
export const safeInput = createSelector(
[input],
@ -43,9 +42,9 @@ export const isShowDifference= isShow(Show.DIFFERENCE)
export const diff = createSelector(
[format, compare],
(format, compare) => {
return Dubdiff.plaintextDiff(compare.original, compare.final)
[format, safeInput],
(format, safeInput) => {
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
/*
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' '))
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => (

View File

@ -31,7 +31,6 @@ function createComparisonWithId(req, res) {
const id = req.params.id
const {a, b} = req.body
return writeRecord(res, id, {a, b, id})
}
@ -82,12 +81,12 @@ function writeRecord(res, id, data) {
module.exports = router
function handleError(res, err) {
console.log(err);
return res.send(500, err);
console.log(err)
return res.send(500, err)
}
// returns a filename for the given comparison
function fnData (id) {
return "./data/" + "id-" + id + ".json";
return `./data/${id}.json`
}

View File

@ -3,6 +3,8 @@ import path from 'path'
import bodyParser from 'body-parser'
import * as Redux from 'redux'
import fetch from 'isomorphic-fetch'
import comparisonRouter from './comparison'
@ -11,6 +13,8 @@ import * as actions from '../common/actions'
import render from './render'
const PORT = 8080
const app = express()
//serve the dist static files at /dist
@ -24,27 +28,40 @@ app.use('/api/compare', comparisonRouter);
//the following routes are for server-side rendering of the app
//eventually, we should render the comparison directly from the server
/*
//we should render the comparison directly from the server
//this is garbage, we should use a robust method for loading comparisons, parallel to how saving works
//comparisons should be loaded isomorphically
app.route('/:comparisonId')
.get((req, res) => {
const store = createSessionStore()
...
//fetch the comparison
fetchComparison(req.params.comparisonId)
.then( ({a,b}) => {
store.dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: a})
store.dispatch({type: 'UPDATE_FINAL_INPUT', data: b})
render(store, req, res)
})
.catch( error => {
//... what to do here?
console.log(`Error fetching comparison with id ${req.params.comparisonId}`, error)
})
})
app.route('/'). ...
*/
app.route('/')
.get((req, res) => {
render(createSessionStore(), req, res)
})
//but for now, let's just render the app and let it fetch comparison data
app.use((req, res) => render(createSessionStore(), req, res))
app.listen(8080, function () {
app.listen(PORT, function () {
console.log('Server listening on port 8080.')
})
//this is pretty much redundant at this point
//creates the session store
function createSessionStore() {
//create the redux store
return Redux.createStore(
@ -53,7 +70,16 @@ function createSessionStore() {
}
function fetchComparison(id) {
const endpointUri = `http://localhost:${PORT}/api/compare/${id}`
const fetchOptions = {
method: 'GET'
}
//dispatch post request
return fetch(endpointUri, fetchOptions)
.then(response => response.json())
}
//router.get('/', controller.index);

View File

@ -1,18 +1,17 @@
import React from 'react'
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'
import { match, RoutingContext } from 'react-router'
import { match, RouterContext } from 'react-router'
import routes from '../common/routes.js'
export default function render(store, req, res) {
console.log(store.getState())
// Send the rendered page back to the client
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(renderError('Routing Error', error.message))
console.log(error)
res.status(500).send(renderError('Routing Error:', error.message))
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
@ -20,7 +19,7 @@ export default function render(store, req, res) {
try {
const html = renderToString(
<Provider store={store}>
<RoutingContext {...renderProps} />
<RouterContext {...renderProps} />
</Provider>
)
@ -30,6 +29,7 @@ export default function render(store, req, res) {
res.status(200).send(appTemplate(html, initialState))
}
catch(ex) {
console.log("Render Exception:",ex)
res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
}