Add initial UI for user-editable securities

This commit is contained in:
Aaron Lindsay 2016-10-26 06:58:14 -04:00
parent a61e460c2f
commit ce6660b575
14 changed files with 689 additions and 10 deletions

View File

@ -19,6 +19,52 @@ function securitiesFetched(securities) {
}
}
function createSecurity() {
return {
type: SecurityConstants.CREATE_SECURITY
}
}
function securityCreated(security) {
return {
type: SecurityConstants.SECURITY_CREATED,
security: security
}
}
function updateSecurity() {
return {
type: SecurityConstants.UPDATE_SECURITY
}
}
function securityUpdated(security) {
return {
type: SecurityConstants.SECURITY_UPDATED,
security: security
}
}
function removeSecurity() {
return {
type: SecurityConstants.REMOVE_SECURITY
}
}
function securityRemoved(securityId) {
return {
type: SecurityConstants.SECURITY_REMOVED,
securityId: securityId
}
}
function securitySelected(securityId) {
return {
type: SecurityConstants.SECURITY_SELECTED,
securityId: securityId
}
}
function fetchAll() {
return function (dispatch) {
dispatch(fetchSecurities());
@ -47,6 +93,88 @@ function fetchAll() {
};
}
function create(security) {
return function (dispatch) {
dispatch(createSecurity());
$.ajax({
type: "POST",
dataType: "json",
url: "security/",
data: {security: security.toJSON()},
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
ErrorActions.serverError(e);
} else {
var s = new Security();
s.fromJSON(data);
dispatch(securityCreated(s));
}
},
error: function(jqXHR, status, error) {
ErrorActions.ajaxError(e);
}
});
};
}
function update(security) {
return function (dispatch) {
dispatch(updateSecurity());
$.ajax({
type: "PUT",
dataType: "json",
url: "security/"+security.SecurityId+"/",
data: {security: security.toJSON()},
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
ErrorActions.serverError(e);
} else {
var s = new Security();
s.fromJSON(data);
dispatch(securityUpdated(s));
}
},
error: function(jqXHR, status, error) {
ErrorActions.ajaxError(e);
}
});
};
}
function remove(security) {
return function(dispatch) {
dispatch(removeSecurity());
$.ajax({
type: "DELETE",
dataType: "json",
url: "security/"+security.SecurityId+"/",
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
ErrorActions.serverError(e);
} else {
dispatch(securityRemoved(security.SecurityId));
}
},
error: function(jqXHR, status, error) {
ErrorActions.ajaxError(e);
}
});
};
}
module.exports = {
fetchAll: fetchAll
fetchAll: fetchAll,
create: create,
update: update,
remove: remove,
select: securitySelected
};

View File

@ -0,0 +1,62 @@
var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants');
var ErrorActions = require('./ErrorActions');
var models = require('../models.js');
var Security = models.Security;
var Error = models.Error;
function searchSecurityTemplates(searchString, searchType) {
return {
type: SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES,
searchString: searchString,
searchType: searchType
}
}
function securityTemplatesSearched(searchString, searchType, securities) {
return {
type: SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED,
searchString: searchString,
searchType: searchType,
securities: securities
}
}
function search(searchString, searchType, limit) {
return function (dispatch) {
dispatch(searchSecurityTemplates(searchString, searchType));
if (searchString == "")
return;
$.ajax({
type: "GET",
dataType: "json",
url: "securitytemplate/?search="+searchString+"&type="+searchType+"&limit="+limit,
success: function(data, status, jqXHR) {
var e = new Error();
e.fromJSON(data);
if (e.isError()) {
ErrorActions.serverError(e);
} else if (data.securities == null) {
dispatch(securityTemplatesSearched(searchString, searchType, new Array()));
} else {
dispatch(securityTemplatesSearched(searchString, searchType,
data.securities.map(function(json) {
var s = new Security();
s.fromJSON(json);
return s;
})));
}
},
error: function(jqXHR, status, error) {
ErrorActions.ajaxError(e);
}
});
};
}
module.exports = {
search: search
};

View File

@ -15,7 +15,6 @@ var ButtonGroup = ReactBootstrap.ButtonGroup;
var Glyphicon = ReactBootstrap.Glyphicon;
var ListGroup = ReactBootstrap.ListGroup;
var ListGroupItem = ReactBootstrap.ListGroupItem;
var Collapse = ReactBootstrap.Collapse;
var Alert = ReactBootstrap.Alert;
var Modal = ReactBootstrap.Modal;
var Collapse = ReactBootstrap.Collapse;

View File

@ -10,6 +10,7 @@ var TopBarContainer = require('../containers/TopBarContainer');
var NewUserForm = require('./NewUserForm');
var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer');
var AccountsTabContainer = require('../containers/AccountsTabContainer');
var SecuritiesTabContainer = require('../containers/SecuritiesTabContainer');
module.exports = React.createClass({
displayName: "MoneyGoApp",
@ -67,9 +68,13 @@ module.exports = React.createClass({
<AccountsTabContainer
className="fullheight" />
</Tab>
<Tab title="Scheduled Transactions" eventKey={2} >Scheduled transactions go here...</Tab>
<Tab title="Budgets" eventKey={3} >Budgets go here...</Tab>
<Tab title="Reports" eventKey={4} >Reports go here...</Tab>
<Tab title="Securities" eventKey={2} >
<SecuritiesTabContainer
className="fullheight" />
</Tab>
<Tab title="Scheduled Transactions" eventKey={3} >Scheduled transactions go here...</Tab>
<Tab title="Budgets" eventKey={4} >Budgets go here...</Tab>
<Tab title="Reports" eventKey={5} >Reports go here...</Tab>
</Tabs>);
else
mainContent = (

View File

@ -0,0 +1,363 @@
var React = require('react');
var ReactDOM = require('react-dom');
var ReactBootstrap = require('react-bootstrap');
var Grid = ReactBootstrap.Grid;
var Row = ReactBootstrap.Row;
var Col = ReactBootstrap.Col;
var Form = ReactBootstrap.Form;
var FormGroup = ReactBootstrap.FormGroup;
var FormControl = ReactBootstrap.FormControl;
var ControlLabel = ReactBootstrap.ControlLabel;
var Button = ReactBootstrap.Button;
var ButtonGroup = ReactBootstrap.ButtonGroup;
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
var Glyphicon = ReactBootstrap.Glyphicon;
var ListGroup = ReactBootstrap.ListGroup;
var ListGroupItem = ReactBootstrap.ListGroupItem;
var Modal = ReactBootstrap.Modal;
var Panel = ReactBootstrap.Panel;
var Combobox = require('react-widgets').Combobox;
var models = require('../models');
var Security = models.Security;
var SecurityType = models.SecurityType;
var SecurityTypeList = models.SecurityTypeList;
const SecurityTemplatePanel = React.createClass({
handleSearchChange: function(){
this.props.onSearchTemplates(ReactDOM.findDOMNode(this.refs.search).value, 0, this.props.maxResults + 1);
},
renderTemplateList: function() {
var templates = this.props.securityTemplates;
if (this.props.search != "") {
var items = [];
for (var i = 0; i < templates.length && i < 15; i++) {
var template = templates[i];
var self = this;
var onClickFn = (function() {
var j = i;
return function(){self.props.onSelectTemplate(templates[j])};
})();
var key = template.Type.toString() + template.AlternateId;
items.push((
<ListGroupItem onClick={onClickFn} key={key}>
{template.Name} - {template.Description}
</ListGroupItem>
));
}
if (templates.length > this.props.maxResults) {
items.push((
<ListGroupItem disabled key="too-many-templates">
Too many templates to display, please refine your search...
</ListGroupItem>
));
} else if (templates.length == 0) {
items.push((
<ListGroupItem disabled key="no-templates">
Sorry, no templates matched your search...
</ListGroupItem>
));
}
return (
<div>
<br />
<ControlLabel>Select a template to populate your security:</ControlLabel>
<ListGroup>
{items}
</ListGroup>
</div>
);
}
},
render: function() {
return (
<Panel collapsible header="Populate Security from Template...">
<FormControl type="text"
placeholder="Search..."
value={this.props.search}
onChange={this.handleSearchChange}
ref="search"/>
{this.renderTemplateList()}
</Panel>
);
}
});
const AddEditSecurityModal = React.createClass({
getInitialState: function() {
var s = {
securityid: -1,
name: "",
description: "",
symbol: "",
precision: 0,
type: 1,
alternateid: ""
};
if (this.props.editSecurity != null) {
s.securityid = this.props.editSecurity.SecurityId;
s.name = this.props.editSecurity.Name;
s.description = this.props.editSecurity.Description;
s.symbol = this.props.editSecurity.Symbol;
s.precision = this.props.editSecurity.Precision;
s.type = this.props.editSecurity.Type;
s.alternateid = this.props.editSecurity.AlternateId;
}
return s;
},
onSelectTemplate: function(template) {
this.setState({
name: template.Name,
description: template.Description,
symbol: template.Symbol,
precision: template.Precision,
type: template.Type,
alternateid: template.AlternateId
});
},
handleCancel: function() {
if (this.props.onCancel != null)
this.props.onCancel();
},
handleNameChange: function() {
this.setState({
name: ReactDOM.findDOMNode(this.refs.name).value,
});
},
handleDescriptionChange: function() {
this.setState({
description: ReactDOM.findDOMNode(this.refs.description).value,
});
},
handleSymbolChange: function() {
this.setState({
symbol: ReactDOM.findDOMNode(this.refs.symbol).value,
});
},
handlePrecisionChange: function() {
this.setState({
precision: +ReactDOM.findDOMNode(this.refs.precision).value,
});
},
handleTypeChange: function(type) {
if (type.hasOwnProperty('TypeId'))
this.setState({
type: type.TypeId
});
},
handleAlternateIdChange: function() {
this.setState({
alternateid: ReactDOM.findDOMNode(this.refs.alternateid).value,
});
},
handleSubmit: function() {
var s = new Security();
if (this.props.editSecurity != null)
s.SecurityId = this.state.securityid;
s.Name = this.state.name;
s.Description = this.state.description;
s.Symbol = this.state.symbol;
s.Precision = this.state.precision;
s.Type = this.state.type;
s.AlternateId = this.state.alternateid;
if (this.props.onSubmit != null)
this.props.onSubmit(s);
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.show && !this.props.show) {
this.setState(this.getInitialState());
}
},
render: function() {
var headerText = (this.props.editSecurity != null) ? "Edit" : "Create New";
var buttonText = (this.props.editSecurity != null) ? "Save Changes" : "Create Security";
var alternateidname = (this.state.type == SecurityType.Currency) ? "ISO 4217 Code" : "CUSIP";
return (
<Modal show={this.props.show} onHide={this.handleCancel}>
<Modal.Header closeButton>
<Modal.Title>{headerText} Security</Modal.Title>
</Modal.Header>
<Modal.Body>
<SecurityTemplatePanel
search={this.props.securityTemplates.search}
securityTemplates={this.props.securityTemplates.templates}
onSearchTemplates={this.props.onSearchTemplates}
maxResults={15}
onSelectTemplate={this.onSelectTemplate} />
<Form horizontal onSubmit={this.handleSubmit}>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Name</Col>
<Col xs={9}>
<FormControl type="text"
value={this.state.name}
onChange={this.handleNameChange}
ref="name"/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Description</Col>
<Col xs={9}>
<FormControl type="text"
value={this.state.description}
onChange={this.handleDescriptionChange}
ref="description"/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Symbol or Ticker</Col>
<Col xs={9}>
<FormControl type="text"
value={this.state.symbol}
onChange={this.handleSymbolChange}
ref="symbol"/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Smallest Fraction Traded</Col>
<Col xs={9}>
<FormControl componentClass="select"
placeholder={this.state.precision}
value={this.state.precision}
onChange={this.handlePrecisionChange}
ref="precision">
<option value={0}>1</option>
<option value={1}>0.1 (1/10)</option>
<option value={2}>0.01 (1/100)</option>
<option value={3}>0.001 (1/1000)</option>
<option value={4}>0.0001 (1/10000)</option>
<option value={5}>0.00001 (1/100000)</option>
</FormControl>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Security Type</Col>
<Col xs={9}>
<Combobox
suggest
data={SecurityTypeList}
valueField='TypeId'
textField='Name'
value={this.state.type}
onChange={this.handleTypeChange}
ref="type" />
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>{alternateidname}</Col>
<Col xs={9}>
<FormControl type="text"
value={this.state.alternateid}
onChange={this.handleAlternateIdChange}
ref="alternateid"/>
</Col>
</FormGroup>
</Form>
</Modal.Body>
<Modal.Footer>
<ButtonGroup className="pull-right">
<Button onClick={this.handleCancel} bsStyle="warning">Cancel</Button>
<Button onClick={this.handleSubmit} bsStyle="success">{buttonText}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
}
});
const SecurityList = React.createClass({
render: function() {
var children = [];
var self = this;
for (var securityId in this.props.securities) {
if (this.props.securities.hasOwnProperty(securityId)) {
var buttonStyle = (securityId == this.props.selectedSecurity) ? "info" : "link";
var onClickFn = (function() {
var id = securityId;
return function(){self.props.onSelectSecurity(id)};
})();
children.push((<Button
bsStyle={buttonStyle}
key={securityId}
onClick={onClickFn}>
{this.props.securities[securityId].Name} - {this.props.securities[securityId].Description}
</Button>));
}
}
return (
<div>
{children}
</div>
);
}
});
module.exports = React.createClass({
displayName: "SecuritiesTab",
getInitialState: function() {
return {
creatingNewSecurity: false,
editingSecurity: false
};
},
handleNewSecurity: function() {
this.setState({creatingNewSecurity: true});
},
handleEditSecurity: function() {
this.setState({editingSecurity: true});
},
handleCreationCancel: function() {
this.setState({creatingNewSecurity: false});
},
handleCreationSubmit: function(security) {
this.setState({creatingNewSecurity: false});
this.props.onCreateSecurity(security);
},
handleEditingCancel: function() {
this.setState({editingSecurity: false});
},
handleEditingSubmit: function(security) {
this.setState({editingSecurity: false});
this.props.onUpdateSecurity(security);
},
render: function() {
var editDisabled = this.props.selectedSecurity == -1;
var selectedSecurity = null;
if (this.props.securities.hasOwnProperty(this.props.selectedSecurity))
selectedSecurity = this.props.securities[this.props.selectedSecurity];
return (
<Grid fluid className="fullheight"><Row className="fullheight">
<Col xs={3} className="fullheight securitylist-column">
<AddEditSecurityModal
show={this.state.creatingNewSecurity}
onCancel={this.handleCreationCancel}
onSubmit={this.handleCreationSubmit}
onSearchTemplates={this.props.onSearchTemplates}
securityTemplates={this.props.securityTemplates} />
<AddEditSecurityModal
show={this.state.editingSecurity}
editSecurity={selectedSecurity}
onCancel={this.handleEditingCancel}
onSubmit={this.handleEditingSubmit}
onSearchTemplates={this.props.onSearchTemplates}
securityTemplates={this.props.securityTemplates} />
<SecurityList
selectedSecurity={this.props.selectedSecurity}
securities={this.props.securities}
onSelectSecurity={this.props.onSelectSecurity} />
</Col><Col xs={9} className="fullheight securities-column">
<ButtonToolbar className="pull-right"><ButtonGroup>
<Button onClick={this.handleEditSecurity} bsStyle="primary" disabled={editDisabled}><Glyphicon glyph='cog'/> Edit Security</Button>
<Button onClick={this.handleNewSecurity} bsStyle="success"><Glyphicon glyph='plus-sign'/> New Security</Button>
</ButtonGroup></ButtonToolbar>
</Col>
</Row></Grid>
);
}
});

View File

@ -8,5 +8,6 @@ module.exports = keyMirror({
UPDATE_SECURITY: null,
SECURITY_UPDATED: null,
REMOVE_SECURITY: null,
SECURITY_REMOVED: null
SECURITY_REMOVED: null,
SECURITY_SELECTED: null
});

View File

@ -0,0 +1,6 @@
var keyMirror = require('keymirror');
module.exports = keyMirror({
SEARCH_SECURITY_TEMPLATES: null,
SECURITY_TEMPLATES_SEARCHED: null
});

View File

@ -0,0 +1,35 @@
var connect = require('react-redux').connect;
var SecurityActions = require('../actions/SecurityActions');
var SecurityTemplateActions = require('../actions/SecurityTemplateActions');
var SecuritiesTab = require('../components/SecuritiesTab');
function mapStateToProps(state) {
var selectedSecurityAccounts = [];
for (var accountId in state.accounts) {
if (state.accounts.hasOwnProperty(accountId)
&& state.accounts[accountId].SecurityId == state.selectedSecurity)
selectedSecurityAccounts.push(state.accounts[accountId]);
}
return {
securities: state.securities,
selectedSecurityAccounts: selectedSecurityAccounts,
selectedSecurity: state.selectedSecurity,
securityTemplates: state.securityTemplates
}
}
function mapDispatchToProps(dispatch) {
return {
onCreateSecurity: function(security) {dispatch(SecurityActions.create(security))},
onUpdateSecurity: function(security) {dispatch(SecurityActions.update(security))},
onDeleteSecurity: function(securityId) {dispatch(SecurityActions.remove(securityId))},
onSelectSecurity: function(securityId) {dispatch(SecurityActions.select(securityId))},
onSearchTemplates: function(search, type, limit) {dispatch(SecurityTemplateActions.search(search, type, limit))}
}
}
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(SecuritiesTab)

View File

@ -94,6 +94,7 @@ function Security() {
this.Symbol = "";
this.Precision = -1;
this.Type = -1;
this.AlternateId = "";
}
Security.prototype.toJSON = function() {
@ -104,6 +105,7 @@ Security.prototype.toJSON = function() {
json_obj.Symbol = this.Symbol;
json_obj.Precision = this.Precision;
json_obj.Type = this.Type;
json_obj.AlternateId = this.AlternateId;
return JSON.stringify(json_obj);
}
@ -122,6 +124,8 @@ Security.prototype.fromJSON = function(json_input) {
this.Precision = json_obj.Precision;
if (json_obj.hasOwnProperty("Type"))
this.Type = json_obj.Type;
if (json_obj.hasOwnProperty("AlternateId"))
this.AlternateId = json_obj.AlternateId;
}
Security.prototype.isSecurity = function() {

View File

@ -4,7 +4,9 @@ var UserReducer = require('./UserReducer');
var SessionReducer = require('./SessionReducer');
var AccountReducer = require('./AccountReducer');
var SecurityReducer = require('./SecurityReducer');
var SecurityTemplateReducer = require('./SecurityTemplateReducer');
var SelectedAccountReducer = require('./SelectedAccountReducer');
var SelectedSecurityReducer = require('./SelectedSecurityReducer');
var ErrorReducer = require('./ErrorReducer');
module.exports = Redux.combineReducers({
@ -12,6 +14,8 @@ module.exports = Redux.combineReducers({
session: SessionReducer,
accounts: AccountReducer,
securities: SecurityReducer,
securityTemplates: SecurityTemplateReducer,
selectedAccount: SelectedAccountReducer,
selectedSecurity: SelectedSecurityReducer,
error: ErrorReducer
});

View File

@ -0,0 +1,33 @@
var assign = require('object-assign');
var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants');
var UserConstants = require('../constants/UserConstants');
var SecurityType = require('../models').SecurityType;
module.exports = function(state = {search: "", type: 0, templates: [], searchNumber: 0}, action) {
switch (action.type) {
case SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES:
return {
search: action.searchString,
type: action.searchType,
templates: []
};
case SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED:
if ((action.searchString != state.search) || (action.searchType != state.type))
return state;
return {
search: action.searchString,
type: action.searchType,
templates: action.securities
};
case UserConstants.USER_LOGGEDOUT:
return {
search: "",
type: 0,
templates: []
};
default:
return state;
}
};

View File

@ -0,0 +1,23 @@
var SecurityConstants = require('../constants/SecurityConstants');
var UserConstants = require('../constants/UserConstants');
module.exports = function(state = -1, action) {
switch (action.type) {
case SecurityConstants.SECURITIES_FETCHED:
for (var i = 0; i < action.securities.length; i++) {
if (action.securities[i].SecurityId == state)
return state;
}
return -1;
case SecurityConstants.SECURITY_REMOVED:
if (action.securityId == state)
return -1;
return state;
case SecurityConstants.SECURITY_SELECTED:
return action.securityId;
case UserConstants.USER_LOGGEDOUT:
return -1;
default:
return state;
}
};

View File

@ -7,6 +7,7 @@ import (
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
@ -58,7 +59,7 @@ func (sl *SecurityList) Write(w http.ResponseWriter) error {
return enc.Encode(sl)
}
func SearchSecurityTemplates(search string, _type int64) []*Security {
func SearchSecurityTemplates(search string, _type int64, limit int64) []*Security {
upperSearch := strings.ToUpper(search)
var results []*Security
for i, security := range SecurityTemplates {
@ -67,6 +68,9 @@ func SearchSecurityTemplates(search string, _type int64) []*Security {
strings.Contains(strings.ToUpper(security.Symbol), upperSearch) {
if _type == 0 || _type == security.Type {
results = append(results, &SecurityTemplates[i])
if limit != -1 && int64(len(results)) >= limit {
break
}
}
}
}
@ -298,10 +302,22 @@ func SecurityTemplateHandler(w http.ResponseWriter, r *http.Request) {
var sl SecurityList
query, _ := url.ParseQuery(r.URL.RawQuery)
var limit int64 = -1
search := query.Get("search")
_type := GetSecurityType(query.Get("type"))
securities := SearchSecurityTemplates(search, _type)
limitstring := query.Get("limit")
if limitstring != "" {
limitint, err := strconv.ParseInt(limitstring, 10, 0)
if err != nil {
WriteError(w, 3 /*Invalid Request*/)
return
}
limit = limitint
}
securities := SearchSecurityTemplates(search, _type, limit)
sl.Securities = &securities
err := (&sl).Write(w)

View File

@ -61,7 +61,7 @@ div.accounttree-root div {
height: 100%-100px;
overflow: auto;
}
.account-column {
.account-column, .securitylist-column {
padding: 15px 15px 43px 15px;
border-right: 1px solid #DDD;
border-left: 1px solid #DDD;
@ -82,7 +82,7 @@ div.accounttree-root div {
height: 100%;
overflow: auto;
}
.transactions-column {
.transactions-column, .securities-column {
padding: 15px;
border-right: 1px solid #DDD;
}