From 70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 17:03:50 -0700 Subject: [PATCH] web: refactor ContentLoader --- web/package.json | 10 +- web/src/js/components/ContentView.jsx | 35 ++----- .../{common => ContentView}/CodeEditor.jsx | 0 .../components/ContentView/ContentLoader.jsx | 97 +++++++++++-------- .../components/ContentView/ContentViews.jsx | 44 +++------ .../ContentView/DownloadContentButton.jsx | 18 ++++ .../ContentView/UploadContentButton.jsx | 28 ++++++ .../components/ContentView/ViewSelector.jsx | 51 +++++++--- web/src/js/utils.js | 1 - 9 files changed, 164 insertions(+), 120 deletions(-) rename web/src/js/components/{common => ContentView}/CodeEditor.jsx (100%) create mode 100644 web/src/js/components/ContentView/DownloadContentButton.jsx create mode 100644 web/src/js/components/ContentView/UploadContentButton.jsx diff --git a/web/package.json b/web/package.json index 81b96adca..302803f24 100644 --- a/web/package.json +++ b/web/package.json @@ -11,15 +11,13 @@ "/src/js" ], "unmockedModulePathPatterns": [ - "react", - "jquery" + "react" ] }, "dependencies": { "bootstrap": "^3.3.6", "classnames": "^2.2.5", "flux": "^2.1.1", - "jquery": "^2.2.3", "lodash": "^4.11.2", "react": "^15.1.0", "react-dom": "^15.1.0", @@ -29,7 +27,7 @@ "redux-logger": "^2.6.1", "redux-thunk": "^2.1.0", "shallowequal": "^0.2.2", - "react-codemirror" : "^0.2.6" + "react-codemirror": "^0.2.6" }, "devDependencies": { "babel-core": "^6.7.7", @@ -55,7 +53,9 @@ "gulp-sourcemaps": "^1.6.0", "gulp-util": "^3.0.7", "jest": "^12.1.1", - "react-addons-test-utils": "^15.1.0", + "react": "^15.2.1", + "react-addons-test-utils": "^15.2.1", + "react-dom": "^15.2.1", "uglifyify": "^3.0.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 80dec0f46..9ec0266b1 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -1,10 +1,11 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { MessageUtils } from '../flow/utils.js' import * as ContentViews from './ContentView/ContentViews' import * as MetaViews from './ContentView/MetaViews' -import ContentLoader from './ContentView/ContentLoader' import ViewSelector from './ContentView/ViewSelector' +import UploadContentButton from './ContentView/UploadContentButton' +import DownloadContentButton from './ContentView/DownloadContentButton' + import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow' ContentView.propTypes = { @@ -18,7 +19,7 @@ ContentView.propTypes = { ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2) function ContentView(props) { - const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, content, readonly } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, readonly } = props if (message.contentLength === 0) { return @@ -35,34 +36,14 @@ function ContentView(props) { const View = ContentViews[contentView] return (
- {View.textView ? ( - - - - ) : ( - - )} + +
  - - - +   - ContentView.fileInput.click()} - title="Upload a file to replace the content." - > - - - ContentView.fileInput = ref} - className="hidden" - type="file" - onChange={e => {if(e.target.files.length > 0) uploadContent(e.target.files[0])}} - /> +
) diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx similarity index 100% rename from web/src/js/components/common/CodeEditor.jsx rename to web/src/js/components/ContentView/CodeEditor.jsx diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index eff82d057..fb022df65 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -1,58 +1,34 @@ import React, { Component, PropTypes } from 'react' import { MessageUtils } from '../../flow/utils.js' -// This is the only place where we use jQuery. -// Remove when possible. -import $ from "jquery" -export default class ContentLoader extends Component { +export default View => class extends React.Component { + + static displayName = View.displayName || View.name + static matches = View.matches static propTypes = { + ...View.propTypes, + content: PropTypes.string, // mark as non-required flow: PropTypes.object.isRequired, message: PropTypes.object.isRequired, } - constructor(props, context) { - super(props, context) - this.state = { content: null, request: null } - } - - requestContent(nextProps) { - if (this.state.request) { - this.state.request.abort() + constructor(props) { + super(props) + this.state = { + content: undefined, + request: undefined, } - - const requestUrl = MessageUtils.getContentURL(nextProps.flow, nextProps.message) - const request = $.get(requestUrl) - - this.setState({ content: null, request }) - - request - .done(content => { - this.setState({ content }) - }) - .fail((xhr, textStatus, errorThrown) => { - if (textStatus === 'abort') { - return - } - this.setState({ content: `AJAX Error: ${textStatus}\r\n${errorThrown}` }) - }) - .always(() => { - this.setState({ request: null }) - }) } componentWillMount() { - this.requestContent(this.props) + this.startRequest(this.props) } componentWillReceiveProps(nextProps) { - let reload = nextProps.message !== this.props.message - let isUserEdit = !nextProps.readonly && nextProps.message.content - - if (isUserEdit) - this.setState({content: nextProps.message.content}) - else if(reload) - this.requestContent(nextProps) + if (nextProps.message.contentHash !== this.props.message.contentHash) { + this.startRequest(nextProps) + } } componentWillUnmount() { @@ -61,15 +37,50 @@ export default class ContentLoader extends Component { } } + startRequest(props) { + if (this.state.request) { + this.state.request.abort() + } + let requestUrl = MessageUtils.getContentURL(props.flow, props.message) + + // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. + let request = new XMLHttpRequest(); + request.addEventListener("load", this.requestComplete.bind(this, request)); + request.addEventListener("error", this.requestFailed.bind(this, request)); + request.open("GET", requestUrl); + request.send(); + this.setState({ request, content: undefined }) + } + + requestComplete(request, e) { + if (request !== this.state.request) { + return // Stale request + } + this.setState({ + content: request.responseText, + request: undefined + }) + } + + requestFailed(request, e) { + if (request !== this.state.request) { + return // Stale request + } + console.error(e) + // FIXME: Better error handling + this.setState({ + content: "Error getting content.", + request: undefined + }) + } + render() { return this.state.content ? ( - React.cloneElement(this.props.children, { - content: this.state.content - }) + ) : (
) } -} +}; diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index b39e545af..a1adebea5 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -1,20 +1,16 @@ import React, { PropTypes } from 'react' import ContentLoader from './ContentLoader' -import { MessageUtils } from '../../flow/utils.js' -import CodeEditor from '../common/CodeEditor' +import { MessageUtils } from '../../flow/utils' +import CodeEditor from './CodeEditor' -const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw] - -ViewImage.regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i -ViewImage.matches = msg => ViewImage.regex.test(MessageUtils.getContentType(msg)) - +const isImage = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i +ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg)) ViewImage.propTypes = { flow: PropTypes.object.isRequired, message: PropTypes.object.isRequired, } - -export function ViewImage({ flow, message }) { +function ViewImage({ flow, message }) { return (
preview @@ -22,26 +18,23 @@ export function ViewImage({ flow, message }) { ) } -ViewRaw.textView = true -ViewRaw.matches = () => true +ViewRaw.matches = () => true ViewRaw.propTypes = { content: React.PropTypes.string.isRequired, } - -export function ViewRaw({ content, readonly, onChange }) { +function ViewRaw({ content, readonly, onChange }) { return readonly ?
{content}
: } +ViewRaw = ContentLoader(ViewRaw) -ViewJSON.textView = true -ViewJSON.regex = /^application\/json$/i -ViewJSON.matches = msg => ViewJSON.regex.test(MessageUtils.getContentType(msg)) +const isJSON = /^application\/json$/i +ViewJSON.matches = msg => isJSON.test(MessageUtils.getContentType(msg)) ViewJSON.propTypes = { content: React.PropTypes.string.isRequired, } - -export function ViewJSON({ content }) { +function ViewJSON({ content }) { let json = content try { json = JSON.stringify(JSON.parse(content), null, 2); @@ -50,23 +43,18 @@ export function ViewJSON({ content }) { } return
{json}
} +ViewJSON = ContentLoader(ViewJSON) ViewAuto.matches = () => false -ViewAuto.findView = msg => views.find(v => v.matches(msg)) || views[views.length - 1] - +ViewAuto.findView = msg => [ViewImage, ViewJSON, ViewRaw].find(v => v.matches(msg)) || ViewRaw ViewAuto.propTypes = { message: React.PropTypes.object.isRequired, flow: React.PropTypes.object.isRequired, } - -export function ViewAuto({ message, flow, readonly, onChange }) { +function ViewAuto({ message, flow, readonly, onChange }) { const View = ViewAuto.findView(message) - if (View.textView) { - return - } else { - return - } + return } -export default views +export { ViewImage, ViewRaw, ViewAuto, ViewJSON } diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx new file mode 100644 index 000000000..3f11f9097 --- /dev/null +++ b/web/src/js/components/ContentView/DownloadContentButton.jsx @@ -0,0 +1,18 @@ +import { MessageUtils } from "../../flow/utils" +import { PropTypes } from 'react' + +DownloadContentButton.propTypes = { + flow: PropTypes.object.isRequired, + message: PropTypes.object.isRequired, +} + +export default function DownloadContentButton({ flow, message }) { + + return ( + + + + ) +} diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx new file mode 100644 index 000000000..0652b584a --- /dev/null +++ b/web/src/js/components/ContentView/UploadContentButton.jsx @@ -0,0 +1,28 @@ +import { PropTypes } from 'react' + +UploadContentButton.propTypes = { + uploadContent: PropTypes.func.isRequired, +} + +export default function UploadContentButton({ uploadContent }) { + + let fileInput; + + return ( + fileInput.click()} + title="Upload a file to replace the content."> + + fileInput = ref} + className="hidden" + type="file" + onChange={e => { + if (e.target.files.length > 0) uploadContent(e.target.files[0]) + }} + /> + + + ) +} + diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 9b151a5b1..973d23332 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -1,28 +1,47 @@ import React, { PropTypes } from 'react' import classnames from 'classnames' -import views, { ViewAuto } from './ContentViews' +import { connect } from 'react-redux' +import * as ContentViews from './ContentViews' +import { setContentView } from "../../ducks/ui/flow"; + + +function ViewButton({ name, setContentView, children, activeView }) { + return ( + + ) +} +ViewButton = connect(state => ({ + activeView: state.ui.flow.contentView +}), { + setContentView +})(ViewButton) + ViewSelector.propTypes = { - active: PropTypes.func.isRequired, message: PropTypes.object.isRequired, - onSelectView: PropTypes.func.isRequired, } +export default function ViewSelector({ message }) { + + let autoView = ContentViews.ViewAuto.findView(message) + let autoViewName = (autoView.displayName || autoView.name) + .toLowerCase() + .replace('view', '') + .replace(/ContentLoader\((.+)\)/,"$1") -export default function ViewSelector({ active, message, onSelectView }) { return (
- {views.map(View => ( - - ))} + + auto: {autoViewName} + + {Object.keys(ContentViews).map(name => + name !== "ViewAuto" && + {name.toLowerCase().replace('view', '')} + )} +
) } diff --git a/web/src/js/utils.js b/web/src/js/utils.js index cc17c565f..e44182d0d 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -124,7 +124,6 @@ export const pure = renderFn => class extends React.Component { static displayName = renderFn.name shouldComponentUpdate(nextProps) { - console.log(!shallowEqual(this.props, nextProps)) return !shallowEqual(this.props, nextProps) }