Created
October 27, 2015 19:15
-
-
Save nwwells/570b0a9bb26fe5f8f0c4 to your computer and use it in GitHub Desktop.
Nathan's work at Symbiont.io
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 8522592042377b46150041e1f723071a9b9016a8 Mon Sep 17 00:00:00 2001 | |
From: Nathan Wells <[email protected]> | |
Date: Tue, 27 Oct 2015 12:06:34 -0400 | |
Subject: [PATCH] renaming Content to content | |
--- | |
components/record/index.js | 8 ++++---- | |
1 file changed, 4 insertions(+), 4 deletions(-) | |
diff --git a/components/record/index.js b/components/record/index.js | |
index 6a06933..8748703 100644 | |
--- a/components/record/index.js | |
+++ b/components/record/index.js | |
@@ -60,11 +60,11 @@ export default React.createClass({ | |
const { data, loading, modifying, editButtons, viewButtons, canEdit, id } = this.props; | |
const fields = this.props.fields.map(addDefaultFieldProperties); | |
- let Content; | |
+ let content; | |
const isReadonly = !canEdit && modifying[id]; | |
if (isReadonly) { | |
- Content = ( | |
+ content = ( | |
<div> | |
<Table striped bordered hover> | |
{ | |
@@ -89,7 +89,7 @@ export default React.createClass({ | |
//(<span>{modifying[field.get]}</span>) : | |
); | |
} else { | |
- Content = ( | |
+ content = ( | |
<form className='form-horizontal'> | |
{ | |
fields.map(field => { | |
@@ -115,7 +115,7 @@ export default React.createClass({ | |
} | |
return ( | |
- <div>{Content}</div> | |
+ <div>{content}</div> | |
); | |
}, | |
-- | |
2.2.1 | |
From 4d6763bc0f79b98890c58f0d272aaf7f25d8d28a Mon Sep 17 00:00:00 2001 | |
From: Nathan Wells <[email protected]> | |
Date: Tue, 27 Oct 2015 15:14:16 -0400 | |
Subject: [PATCH] Implement Loan Details with good abstrations | |
--- | |
actions/loans.js | 53 +++++++++++++++++++++++++++-------- | |
components/record/index.js | 5 +--- | |
components/simple-table.js | 8 ++++-- | |
components/tableditable/index.js | 47 ++++++++++++++++++++++++------- | |
containers/loans.js | 60 +++++++++++++++++++++++++++++----------- | |
utils.js | 8 +++++- | |
6 files changed, 135 insertions(+), 46 deletions(-) | |
diff --git a/actions/loans.js b/actions/loans.js | |
index e5daded..0821a8f 100644 | |
--- a/actions/loans.js | |
+++ b/actions/loans.js | |
@@ -2,7 +2,7 @@ import { get } from 'lodash'; | |
import { getApi } from '../api'; | |
-import { hydrate } from '../utils'; | |
+import { hydrate, resolveProps } from '../utils'; | |
import { | |
CANCEL_LOAN_ACTION, | |
@@ -10,23 +10,52 @@ import { | |
FETCH_LOANS_ASYNC_STATES, | |
EDIT_LOAN_ASYNC_STATES, | |
ADD_LOAN_ASYNC_STATES, | |
- SAVE_LOAN_ASYNC_STATES | |
+ SAVE_LOAN_ASYNC_STATES, | |
} from '../constants/actions'; | |
-const organizationTypes = ['agent', 'borrower', 'issuer', 'lender']; | |
+const loanOrganizationTypes = ['agent', 'borrower', 'issuer', 'lender']; | |
+const assignmentOrganizationTypes = ['assigner', 'borrower', 'lender']; | |
+const lenderOrganizationTypes = ['lender_id']; | |
const getOrganizations = async () => await getApi().get(`/organizations`).then(res => res.data); | |
+const getAssignments = async baseUrl => await getApi().get(`${baseUrl}/assignments`).then(res => res.data); | |
+const getLenders = async baseUrl => await getApi().get(`${baseUrl}/lenders`).then(res => res.data); | |
+ | |
+const getFullyHydratedLoan = async loanId => { | |
+ const url = `/loans/${loanId}`; | |
+ const {loan, organizations, assignments, lenders} = await resolveProps({ | |
+ loan: getApi().get(url).then(res => res.data), | |
+ organizations: getOrganizations(), | |
+ assignments: getAssignments(url), | |
+ lenders: getLenders(url), | |
+ }); | |
+ const hydratedLoan = hydrate({ | |
+ itemOrCollection: loan, | |
+ propertiesToHydrate: loanOrganizationTypes, | |
+ mapping: organizations, | |
+ }); | |
+ const hydratedAssignments = hydrate({ | |
+ itemOrCollection: assignments, | |
+ propertiesToHydrate: assignmentOrganizationTypes, | |
+ mapping: organizations, | |
+ }); | |
+ const hydratedLenders = hydrate({ | |
+ itemOrCollection: lenders, | |
+ propertiesToHydrate: lenderOrganizationTypes, | |
+ mapping: organizations, | |
+ }); | |
+ return { ...hydratedLoan, assignments: hydratedAssignments, lenders: hydratedLenders }; | |
+}; | |
-const getFullyHydratedLoanOrLoans = async loanId => { | |
- const urlSuffix = loanId ? `/${loanId}` : ''; | |
- const url = `/loans${urlSuffix}`; | |
- const [loanOrLoans, organizations] = await Promise.all([ | |
+const getFullyHydratedLoans = async () => { | |
+ const url = `/loans`; | |
+ const [loans, organizations] = await Promise.all([ | |
getApi().get(url).then(res => res.data), | |
getOrganizations(), | |
]); | |
return hydrate({ | |
- itemOrCollection: loanOrLoans, | |
- propertiesToHydrate: organizationTypes, | |
+ itemOrCollection: loans, | |
+ propertiesToHydrate: loanOrganizationTypes, | |
mapping: organizations, | |
}); | |
}; | |
@@ -41,14 +70,14 @@ const groupOrganizationsByType = async () => { | |
export const getLoans = () => ({ | |
types: FETCH_LOANS_ASYNC_STATES, | |
payload: { | |
- records: getFullyHydratedLoanOrLoans(), | |
+ records: getFullyHydratedLoans(), | |
}, | |
}); | |
export const editLoan = ({ loan_id }) => ({ | |
types: EDIT_LOAN_ASYNC_STATES, | |
payload: { | |
- record: getFullyHydratedLoanOrLoans(loan_id), | |
+ record: getFullyHydratedLoan(loan_id), | |
id: loan_id, | |
}, | |
}); | |
@@ -82,7 +111,7 @@ export const saveLoan = record => (dispach, getState) => { | |
dispach({ | |
types: SAVE_LOAN_ASYNC_STATES, | |
payload: { | |
- records: getApi().post('/loans', payload).then(() => getFullyHydratedLoanOrLoans()), | |
+ records: getApi().post('/loans', payload).then(getFullyHydratedLoans), | |
}, | |
}); | |
}; | |
diff --git a/components/record/index.js b/components/record/index.js | |
index 8748703..66dbeac 100644 | |
--- a/components/record/index.js | |
+++ b/components/record/index.js | |
@@ -1,5 +1,6 @@ | |
import React from 'react'; | |
import { get, map, camelCase } from 'lodash'; | |
+import { labelize } from '../../utils'; | |
import { Table, ButtonToolbar, Button, Input as BootstrapInput } from 'react-bootstrap'; | |
@@ -13,10 +14,6 @@ import { | |
const { PropTypes } = React; | |
-const labelize = propName => { | |
- return propName[0].toUpperCase() + camelCase(propName.slice(1)).replace(/[A-Z]/g, ' $&'); | |
-}; | |
- | |
const addDefaultFieldProperties = field => { | |
const obj = typeof field === 'string' ? {} : {...field}; | |
const property = obj.property || field; | |
diff --git a/components/simple-table.js b/components/simple-table.js | |
index e41da3a..e5a6cd8 100644 | |
--- a/components/simple-table.js | |
+++ b/components/simple-table.js | |
@@ -1,12 +1,14 @@ | |
import React from 'react'; | |
+import { get } from 'lodash'; | |
import { Table } from 'react-bootstrap'; | |
+import { labelize } from '../utils'; | |
export default React.createClass({ | |
render() { | |
const { records } = this.props; | |
const fields = this.props.fields.map(field => typeof field === 'object' ? field : { | |
- label: field[0].toUpperCase() + field.slice(1).replace(/[-_]./g, m => m[1].toUpperCase()), | |
- getter(record) { return record[field]; }, | |
+ label: labelize(field), | |
+ render(record) { return get(record, field); }, | |
}); | |
let content; | |
if (!records) { | |
@@ -29,7 +31,7 @@ export default React.createClass({ | |
return ( | |
<tr> | |
{ | |
- fields.map(field => <td>{field.getter(portfolioItem)}</td>) | |
+ fields.map(field => <td>{field.render(portfolioItem)}</td>) | |
} | |
</tr> | |
); | |
diff --git a/components/tableditable/index.js b/components/tableditable/index.js | |
index f6858ca..1188de2 100644 | |
--- a/components/tableditable/index.js | |
+++ b/components/tableditable/index.js | |
@@ -2,6 +2,7 @@ import React from 'react'; | |
import { get, map, camelCase } from 'lodash'; | |
import { Table, Button, Glyphicon, Modal } from 'react-bootstrap'; | |
import { types } from 'zan'; | |
+import { labelize } from '../../utils'; | |
const { string, arrayOf, oneOfType, shape, objectOf, object, func, oneOf, number } = types; | |
@@ -27,8 +28,12 @@ export { | |
Null, | |
}; | |
-const labelize = propName => { | |
- return propName[0].toUpperCase() + camelCase(propName.slice(1)).replace(/[A-Z]/g, ' $&'); | |
+const convertButtonMapToArray = (value, key) => { | |
+ return { | |
+ iconName: null, | |
+ label: labelize(key), | |
+ callback: value, | |
+ }; | |
}; | |
const addDefaultFieldProperties = field => { | |
@@ -112,11 +117,19 @@ export default React.createClass({ | |
this.props.actions.save(record); | |
}, | |
- render() { | |
- | |
+ createDispatcher(record, callback) { | |
+ return e => { | |
+ e.preventDefault(); | |
+ callback(record); | |
+ }; | |
+ }, | |
+ render() { | |
const { data, loading, modifying, actionsLabel } = this.props; | |
const fields = this.props.fields.map(addDefaultFieldProperties); | |
+ const tableButtons = Array.isArray(this.props.tableButtons) ? | |
+ this.props.tableButtons : | |
+ map(this.props.tableButtons, convertButtonMapToArray); | |
let content; | |
@@ -152,12 +165,26 @@ export default React.createClass({ | |
{ | |
record.pending && <div style={cellOverlay}>{record.pending}</div> | |
} | |
- <Button onClick={e => this.editAction(e, record)}> | |
- <Glyphicon glyph='pencil' /> Edit | |
- </Button> | |
- <Button onClick={e => this.deleteAction(e, record)}> | |
- <Glyphicon glyph='trash' /> Delete | |
- </Button> | |
+ { | |
+ tableButtons.map((tableButton, index) => { | |
+ let iconContent; | |
+ if (tableButton.iconName) { | |
+ iconContent = ( | |
+ <span> | |
+ <Glyphicon glyph={tableButton.iconName} />{' '} | |
+ </span> | |
+ ); | |
+ } else { | |
+ iconContent = null; | |
+ } | |
+ return ( | |
+ <Button key={index} onClick={this.createDispatcher(record, tableButton.callback)}> | |
+ {iconContent} | |
+ {tableButton.label} | |
+ </Button> | |
+ ); | |
+ }) | |
+ } | |
</th> | |
{ | |
fields.map((field, index) => { | |
diff --git a/containers/loans.js b/containers/loans.js | |
index e1f0575..339cb82 100644 | |
--- a/containers/loans.js | |
+++ b/containers/loans.js | |
@@ -9,6 +9,7 @@ import { numberWithCommas } from '../utils'; | |
import Tableditable, { Input, Checkbox, Select, Datepicker, Null } from '../components/tableditable'; | |
import Record from '../components/record'; | |
+import SimpleTable from '../components/simple-table'; | |
import { addLoan, editLoan, deleteLoan, cancelAction, saveLoan } from '../actions/loans'; | |
@@ -18,7 +19,6 @@ const Component = React.createClass({ | |
const { dispatch, records, creatingOrEditing } = this.props; | |
const agents = get(this, 'props.agents', []).map(org => ({ label: org.display_name, value: org.id })); | |
- const lenders = get(this, 'props.lenders', []).map(org => ({ label: org.display_name, value: org.id })); | |
const borrowers = get(this, 'props.borrowers', []).map(org => ({ label: org.display_name, value: org.id })); | |
const issuers = get(this, 'props.issuers', []).map(org => ({ label: org.display_name, value: org.id })); | |
@@ -30,11 +30,9 @@ const Component = React.createClass({ | |
const fields = [ | |
{ property: 'loan_id', label: 'ID', Component: Null }, | |
- //{ property: 'asset_type', label: 'Type', Component: Select, options: [{label: 'Loan', value: 'loan'}] }, | |
{ property: 'agent.display_name', label: 'Agent', Component: Null }, | |
{ get: 'borrower.display_name', set: 'borrower', label: 'Borrower', Component: Select, options: borrowers }, | |
{ property: 'issuer.display_name', label: 'Issuer', Component: Null }, | |
- //{ property: 'lender.display_name', label: 'Lender', Component: Null }, | |
{ get: 'quantity', set: 'loan_quantity', label: 'Quantity', render: record => numberWithCommas(record.quantity) }, | |
{ set: 'loan_currency', get: 'loan_currency_code', label: 'Currency', Component: Select, options: currencies }, | |
{ property: 'maturity', label: 'Maturity', Component: Datepicker, render: renderDate('maturity') }, | |
@@ -42,7 +40,9 @@ const Component = React.createClass({ | |
{ property: 'payment_frequency', Component: Select, options: frequencies, render: renderFrequency }, | |
]; | |
- | |
+ const tableButtons = [ | |
+ { label: 'View Details', iconName: 'th-list' , callback: record => dispatch(editLoan(record)) }, | |
+ ]; | |
const Viewer = creatingOrEditing.record ? Record : Tableditable; | |
@@ -86,19 +86,47 @@ const Component = React.createClass({ | |
}, | |
}; | |
+ const assignmentFields = [ | |
+ 'assignment_id', | |
+ 'assigner.display_name', | |
+ 'borrower.display_name', | |
+ 'borrower_response', | |
+ 'lender.display_name', | |
+ 'lender_response', | |
+ 'lender_quantity', | |
+ 'status', | |
+ ]; | |
+ | |
+ const lenderFields = [ | |
+ 'lender_id.display_name', | |
+ 'quantity', | |
+ ]; | |
- return <Viewer | |
- canEdit={false} | |
- id="loan_id" | |
- fields={fields} | |
- actions={actions} | |
- viewButtons={viewButtons} | |
- editButtons={editButtons} | |
- data={records} | |
- loading="Loading..." | |
- modifying={ get(creatingOrEditing, 'record') } | |
- modalTitle={ get(creatingOrEditing, 'record.id') ? 'Editing ' + creatingOrEditing.loan.display_name : 'Adding new user'} | |
- />; | |
+ const assignments = get(creatingOrEditing, 'record.assignments'); | |
+ const assignmentTable = assignments ? <SimpleTable records={assignments} fields={assignmentFields} /> : null; | |
+ | |
+ const lenders = get(creatingOrEditing, 'record.lenders'); | |
+ const lenderTable = lenders ? <SimpleTable records={lenders} fields={lenderFields} /> : null; | |
+ | |
+ return <div> | |
+ <Viewer | |
+ canEdit={false} | |
+ id="loan_id" | |
+ fields={fields} | |
+ actions={actions} | |
+ viewButtons={viewButtons} | |
+ editButtons={editButtons} | |
+ tableButtons={tableButtons} | |
+ data={records} | |
+ loading="Loading..." | |
+ modifying={ get(creatingOrEditing, 'record') } | |
+ modalTitle={ get(creatingOrEditing, 'record.id') ? 'Editing ' + creatingOrEditing.loan.display_name : 'Adding new user'} | |
+ /> | |
+ <h3>Assignments</h3> | |
+ { assignmentTable } | |
+ <h3>Lenders</h3> | |
+ { lenderTable } | |
+ </div>; | |
}, | |
}); | |
diff --git a/utils.js b/utils.js | |
index d83a29b..e66ba43 100644 | |
--- a/utils.js | |
+++ b/utils.js | |
@@ -1,4 +1,4 @@ | |
-import { find, map, indexBy } from 'lodash'; | |
+import { find, map, indexBy, camelCase } from 'lodash'; | |
export const findIndex = (collection, predicate) => { | |
const found = find(collection, predicate); | |
@@ -30,3 +30,9 @@ export const hydrate = ({ itemOrCollection, propertiesToHydrate, mapping, id = ' | |
}); | |
return isCollection ? hydrated : hydrated[0]; | |
}; | |
+ | |
+ | |
+export const labelize = path => { | |
+ const propName = path.split('.')[0]; | |
+ return propName[0].toUpperCase() + camelCase(propName.slice(1)).replace(/[A-Z]/g, ' $&'); | |
+}; | |
-- | |
2.2.1 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment