aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/components/ContentView.jsx67
-rw-r--r--web/src/js/components/ContentView/CodeEditor.jsx21
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx104
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx45
-rw-r--r--web/src/js/components/ContentView/DownloadContentButton.jsx18
-rw-r--r--web/src/js/components/ContentView/MetaViews.jsx18
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx28
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx51
-rw-r--r--web/src/js/components/FlowView/Headers.jsx3
-rw-r--r--web/src/js/components/FlowView/Messages.jsx21
-rw-r--r--web/src/js/components/FlowView/ToggleEdit.jsx7
-rw-r--r--web/src/js/components/ValueEditor/ValueEditor.jsx2
-rw-r--r--web/src/js/components/common/CodeEditor.jsx30
-rw-r--r--web/src/js/ducks/flows.js5
-rw-r--r--web/src/js/ducks/ui/flow.js37
-rw-r--r--web/src/js/utils.js13
16 files changed, 276 insertions, 194 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
index f7eafc89..75662509 100644
--- a/web/src/js/components/ContentView.jsx
+++ b/web/src/js/components/ContentView.jsx
@@ -1,12 +1,12 @@
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'
-import CodeEditor from './common/CodeEditor'
ContentView.propTypes = {
// It may seem a bit weird at the first glance:
@@ -19,61 +19,32 @@ 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, onContentChange, isFlowEditorOpen, setModifiedFlowContent } = props
+ const { flow, message, contentView, isDisplayLarge, displayLarge, uploadContent, onContentChange, readonly } = props
- if (message.contentLength === 0) {
+ if (message.contentLength === 0 && readonly) {
return <MetaViews.ContentEmpty {...props}/>
}
- if (message.contentLength === null) {
+ if (message.contentLength === null && readonly) {
return <MetaViews.ContentMissing {...props}/>
}
- if (!displayLarge && ContentView.isContentTooLarge(message)) {
+ if (!isDisplayLarge && ContentView.isContentTooLarge(message)) {
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
}
const View = ContentViews[contentView]
-
return (
<div>
- {isFlowEditorOpen ? (
- <ContentLoader flow={flow} message={message}>
- <CodeEditor content="" onChange={content =>{setModifiedFlowContent(content)}}/>
- </ContentLoader>
- ): (
- <div>
- {View.textView ? (
- <ContentLoader flow={flow} message={message}>
- <View content="" />
- </ContentLoader>
- ) : (
- <View flow={flow} message={message} />
- )}
- <div className="view-options text-center">
- <ViewSelector onSelectView={selectView} active={View} message={message}/>
- &nbsp;
- <a className="btn btn-default btn-xs"
- href={MessageUtils.getContentURL(flow, message)}
- title="Download the content of the flow.">
- <i className="fa fa-download"/>
- </a>
- &nbsp;
- <a className="btn btn-default btn-xs"
- onClick={() => ContentView.fileInput.click()}
- title="Upload a file to replace the content."
- >
- <i className="fa fa-upload"/>
- </a>
- <input
- ref={ref => ContentView.fileInput = ref}
- className="hidden"
- type="file"
- onChange={e => {if(e.target.files.length > 0) onContentChange(e.target.files[0])}}
- />
- </div>
- </div>
- )}
+ <View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/>
+
+ <div className="view-options text-center">
+ <ViewSelector message={message}/>
+ &nbsp;
+ <DownloadContentButton flow={flow} message={message}/>
+ &nbsp;
+ <UploadContentButton uploadContent={uploadContent}/>
+ </div>
</div>
)
}
@@ -81,12 +52,10 @@ function ContentView(props) {
export default connect(
state => ({
contentView: state.ui.flow.contentView,
- displayLarge: state.ui.flow.displayLarge,
- isFlowEditorOpen : !!state.ui.flow.modifiedFlow // FIXME
+ isDisplayLarge: state.ui.flow.displayLarge,
}),
{
- selectView: setContentView,
displayLarge,
- updateEdit,
+ updateEdit
}
)(ContentView)
diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx
new file mode 100644
index 00000000..95f1b98b
--- /dev/null
+++ b/web/src/js/components/ContentView/CodeEditor.jsx
@@ -0,0 +1,21 @@
+import React, { Component, PropTypes } from 'react'
+import { render } from 'react-dom';
+import Codemirror from 'react-codemirror';
+
+
+CodeEditor.propTypes = {
+ content: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+export default function CodeEditor ( { content, onChange} ){
+
+ let options = {
+ lineNumbers: true
+ };
+ return (
+ <div onKeyDown={e => e.stopPropagation()}>
+ <Codemirror value={content} onChange={onChange} options={options}/>
+ </div>
+ )
+}
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
index 1a23325c..ba6702ca 100644
--- a/web/src/js/components/ContentView/ContentLoader.jsx
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -1,53 +1,36 @@
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.updateContent(this.props)
}
componentWillReceiveProps(nextProps) {
- if (nextProps.message !== this.props.message) {
- this.requestContent(nextProps)
+ if (
+ nextProps.message.content !== this.props.message.content ||
+ nextProps.message.contentHash !== this.props.message.contentHash
+ ) {
+ this.updateContent(nextProps)
}
}
@@ -57,15 +40,58 @@ export default class ContentLoader extends Component {
}
}
+ updateContent(props) {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
+ // We have a few special cases where we do not need to make an HTTP request.
+ if(props.message.content !== undefined) {
+ return this.setState({request: undefined, content: props.message.content})
+ }
+ if(props.message.contentLength === 0 || props.message.contentLength === null){
+ return this.setState({request: undefined, content: ""})
+ }
+
+ 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
- })
+ return this.state.content !== undefined ? (
+ <View content={this.state.content} {...this.props}/>
) : (
<div className="text-center">
<i className="fa fa-spinner fa-spin"></i>
</div>
)
}
-}
+};
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index 82ee0adc..a1adebea 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -1,19 +1,16 @@
import React, { PropTypes } from 'react'
import ContentLoader from './ContentLoader'
-import { MessageUtils } from '../../flow/utils.js'
+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 (
<div className="flowview-image">
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
@@ -21,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 }) {
- return <pre>{content}</pre>
+function ViewRaw({ content, readonly, onChange }) {
+ return readonly ? <pre>{content}</pre> : <CodeEditor content={content} onChange={onChange}/>
}
+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);
@@ -49,23 +43,18 @@ export function ViewJSON({ content }) {
}
return <pre>{json}</pre>
}
+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 }) {
+function ViewAuto({ message, flow, readonly, onChange }) {
const View = ViewAuto.findView(message)
- if (View.textView) {
- return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
- } else {
- return <View message={message} flow={flow} />
- }
+ return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/>
}
-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 00000000..3f11f909
--- /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 (
+ <a className="btn btn-default btn-xs"
+ href={MessageUtils.getContentURL(flow, message)}
+ title="Download the content of the flow.">
+ <i className="fa fa-download"/>
+ </a>
+ )
+}
diff --git a/web/src/js/components/ContentView/MetaViews.jsx b/web/src/js/components/ContentView/MetaViews.jsx
index 2d064b54..b926738e 100644
--- a/web/src/js/components/ContentView/MetaViews.jsx
+++ b/web/src/js/components/ContentView/MetaViews.jsx
@@ -1,5 +1,7 @@
import React from 'react'
import { formatSize } from '../../utils.js'
+import UploadContentButton from './UploadContentButton'
+import DownloadContentButton from './DownloadContentButton'
export function ContentEmpty({ flow, message }) {
return (
@@ -17,11 +19,19 @@ export function ContentMissing({ flow, message }) {
)
}
-export function ContentTooLarge({ message, onClick }) {
+export function ContentTooLarge({ message, onClick, uploadContent, flow }) {
return (
- <div className="alert alert-warning">
- <button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
- {formatSize(message.contentLength)} content size.
+ <div>
+ <div className="alert alert-warning">
+
+ <button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
+ {formatSize(message.contentLength)} content size.
+ </div>
+ <div className="view-options text-center">
+ <UploadContentButton uploadContent={uploadContent}/>
+ &nbsp;
+ <DownloadContentButton flow={flow} message={message}/>
+ </div>
</div>
)
}
diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx
new file mode 100644
index 00000000..0652b584
--- /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 (
+ <a className="btn btn-default btn-xs"
+ onClick={() => fileInput.click()}
+ title="Upload a file to replace the content.">
+ <i className="fa fa-upload"/>
+ <input
+ ref={ref => fileInput = ref}
+ className="hidden"
+ type="file"
+ onChange={e => {
+ if (e.target.files.length > 0) uploadContent(e.target.files[0])
+ }}
+ />
+ </a>
+
+ )
+}
+
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
index 9b151a5b..89b36231 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 (
+ <button
+ onClick={() => setContentView(name)}
+ className={classnames('btn btn-default', { active: name === activeView })}>
+ {children}
+ </button>
+ )
+}
+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 (
<div className="view-selector btn-group btn-group-xs">
- {views.map(View => (
- <button
- key={View.name}
- onClick={() => onSelectView(View.name)}
- className={classnames('btn btn-default', { active: View === active })}>
- {View === ViewAuto ? (
- `auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}`
- ) : (
- View.name.toLowerCase().replace('view', '')
- )}
- </button>
- ))}
+
+ <ViewButton name="ViewAuto">auto: {autoViewName}</ViewButton>
+
+ {Object.keys(ContentViews).map(name =>
+ name !== "ViewAuto" &&
+ <ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton>
+ )}
+
</div>
)
}
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
index 706dd404..2e181383 100644
--- a/web/src/js/components/FlowView/Headers.jsx
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -126,7 +126,8 @@ export default class Headers extends Component {
onDone={val => this.onChange(i, 0, val)}
onRemove={event => this.onRemove(i, 0, event)}
onTab={event => this.onTab(i, 0, event)}
- />:
+ />
+ <span className="header-colon">:</span>
</td>
<td className="header-value">
<HeaderEditor
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index 133b2883..9de25b5b 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -10,6 +10,7 @@ import ValueEditor from '../ValueEditor/ValueEditor'
import Headers from './Headers'
import { startEdit, updateEdit } from '../../ducks/ui/flow'
+import * as FlowActions from '../../ducks/flows'
import ToggleEdit from './ToggleEdit'
function RequestLine({ flow, readonly, updateFlow }) {
@@ -73,12 +74,13 @@ const Message = connect(
}),
{
updateFlow: updateEdit,
+ uploadContent: FlowActions.uploadContent
}
)
export class Request extends Component {
render() {
- const { flow, isEdit, updateFlow } = this.props
+ const { flow, isEdit, updateFlow, uploadContent } = this.props
return (
<section className="request">
@@ -94,7 +96,12 @@ export class Request extends Component {
/>
<hr/>
- <ContentView flow={flow} message={flow.request}/>
+ <ContentView
+ readonly={!isEdit}
+ flow={flow}
+ onContentChange={content => updateFlow({ request: {content}})}
+ uploadContent={content => uploadContent(flow, content, "request")}
+ message={flow.request}/>
</section>
)
}
@@ -129,7 +136,7 @@ Request = Message(Request)
export class Response extends Component {
render() {
- const { flow, isEdit, updateFlow } = this.props
+ const { flow, isEdit, updateFlow, uploadContent } = this.props
return (
<section className="response">
@@ -144,7 +151,13 @@ export class Response extends Component {
onChange={headers => updateFlow({ response: { headers } })}
/>
<hr/>
- <ContentView flow={flow} message={flow.response}/>
+ <ContentView
+ readonly={!isEdit}
+ flow={flow}
+ onContentChange={content => updateFlow({ response: {content}})}
+ uploadContent={content => uploadContent(flow, content, "response")}
+ message={flow.response}
+ />
</section>
)
}
diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx
index 0c8cbbd8..9016348e 100644
--- a/web/src/js/components/FlowView/ToggleEdit.jsx
+++ b/web/src/js/components/FlowView/ToggleEdit.jsx
@@ -10,11 +10,11 @@ ToggleEdit.propTypes = {
stopEdit: PropTypes.func.isRequired,
}
-function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) {
+function ToggleEdit({ isEdit, startEdit, stopEdit, flow, modifiedFlow }) {
return (
<div className="edit-flow-container">
{isEdit ?
- <a className="edit-flow" onClick={() => stopEdit(flow)}>
+ <a className="edit-flow" onClick={() => stopEdit(flow, modifiedFlow)}>
<i className="fa fa-check"/>
</a>
:
@@ -29,7 +29,8 @@ function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) {
export default connect(
state => ({
isEdit: !!state.ui.flow.modifiedFlow,
- flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]
+ modifiedFlow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]],
+ flow: state.flows.byId[state.flows.selected[0]]
}),
{
startEdit,
diff --git a/web/src/js/components/ValueEditor/ValueEditor.jsx b/web/src/js/components/ValueEditor/ValueEditor.jsx
index dd9c2cde..852f82c4 100644
--- a/web/src/js/components/ValueEditor/ValueEditor.jsx
+++ b/web/src/js/components/ValueEditor/ValueEditor.jsx
@@ -59,7 +59,7 @@ export default class ValueEditor extends Component {
return (
<div
ref={input => this.input = input}
- tabIndex={!this.props.readonly && "0"}
+ tabIndex={this.props.readonly ? undefined : 0}
className={className}
contentEditable={this.state.editable || undefined}
onFocus={this.onFocus}
diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/common/CodeEditor.jsx
deleted file mode 100644
index 5b2305a8..00000000
--- a/web/src/js/components/common/CodeEditor.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { Component, PropTypes } from 'react'
-import { render } from 'react-dom';
-import Codemirror from 'react-codemirror';
-
-
-export default class CodeEditor extends Component{
- static propTypes = {
- content: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- }
-
- constructor(props){
- super(props)
- }
-
- componentWillMount(){
- this.props.onChange(this.props.content)
- }
-
- render() {
- let options = {
- lineNumbers: true
- };
- return (
- <div onKeyDown={e => e.stopPropagation()}>
- <Codemirror value={this.props.content} onChange={this.props.onChange} options={options}/>
- </div>
- )
- }
-}
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index f18e48e6..f96653a9 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -112,10 +112,9 @@ export function update(flow, data) {
return dispatch => fetchApi.put(`/flows/${flow.id}`, data)
}
-export function updateContent(flow, file, type) {
+export function uploadContent(flow, file, type) {
const body = new FormData()
- if (typeof file !== File)
- file = new Blob([file], {type: 'plain/text'})
+ file = new Blob([file], {type: 'plain/text'})
body.append('file', file)
return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} )
}
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index b1fe535f..c9435676 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -1,4 +1,6 @@
import * as flowsActions from '../flows'
+import { getDiff } from "../../utils"
+
import _ from 'lodash'
export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
@@ -6,7 +8,7 @@ export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
SET_TAB = "UI_FLOWVIEW_SET_TAB",
START_EDIT = 'UI_FLOWVIEW_START_EDIT',
UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT',
- STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT'
+ UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT'
const defaultState = {
@@ -22,7 +24,7 @@ export default function reducer(state = defaultState, action) {
case START_EDIT:
return {
...state,
- modifiedFlow: action.flow
+ modifiedFlow: action.flow,
}
case UPDATE_EDIT:
@@ -31,12 +33,6 @@ export default function reducer(state = defaultState, action) {
modifiedFlow: _.merge({}, state.modifiedFlow, action.update)
}
- case STOP_EDIT:
- return {
- ...state,
- modifiedFlow: false
- }
-
case flowsActions.SELECT:
return {
...state,
@@ -44,6 +40,21 @@ export default function reducer(state = defaultState, action) {
displayLarge: false,
}
+ case flowsActions.UPDATE:
+ // There is no explicit "stop edit" event.
+ // We stop editing when we receive an update for
+ // the currently edited flow from the server
+ if (action.item.id === state.modifiedFlow.id) {
+ return {
+ ...state,
+ modifiedFlow: false,
+ displayLarge: false,
+ }
+ } else {
+ return state
+ }
+
+
case SET_TAB:
return {
...state,
@@ -87,11 +98,7 @@ export function updateEdit(update) {
return { type: UPDATE_EDIT, update }
}
-export function stopEdit(flow) {
- return (dispatch) => {
- dispatch(flowsActions.update(flow, flow)).then(() => {
- dispatch(flowsActions.updateFlow(flow))
- dispatch({ type: STOP_EDIT })
- })
- }
+export function stopEdit(flow, modifiedFlow) {
+ let diff = getDiff(flow, modifiedFlow)
+ return flowsActions.update(flow, diff)
}
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index eecacfbb..e44182d0 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -108,11 +108,22 @@ fetchApi.put = (url, json, options) => fetchApi(
}
)
+export function getDiff(obj1, obj2) {
+ let result = {...obj2};
+ for(let key in obj1) {
+ if(_.isEqual(obj2[key], obj1[key]))
+ result[key] = undefined
+ else if(!(Array.isArray(obj2[key]) && Array.isArray(obj1[key])) &&
+ typeof obj2[key] == 'object' && typeof obj1[key] == 'object')
+ result[key] = getDiff(obj1[key], obj2[key])
+ }
+ return result
+}
+
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)
}