mirror of https://github.com/fspc/workstand.git
Browse Source
Trigger! * Added members API endpoint. * Stubbed out member table. Search is broken. * Better, faster member search! * Add add member button.feature/python-error-tracking
Drew Larson
8 years ago
12 changed files with 282 additions and 77 deletions
@ -0,0 +1,155 @@ |
|||||
|
import React from 'react'; |
||||
|
import { polyFill } from 'es6-promise'; |
||||
|
import fetch from 'isomorphic-fetch'; |
||||
|
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; |
||||
|
import FlatButton from 'material-ui/FlatButton'; |
||||
|
import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'; |
||||
|
import TextField from 'material-ui/TextField'; |
||||
|
import RaisedButton from 'material-ui/RaisedButton'; |
||||
|
|
||||
|
function checkStatus(response) { |
||||
|
if (response.status >= 200 && response.status < 300) { |
||||
|
return response; |
||||
|
} |
||||
|
const error = new Error(response.statusText); |
||||
|
error.response = response; |
||||
|
throw error; |
||||
|
} |
||||
|
|
||||
|
function parseJSON(response) { |
||||
|
return response.json(); |
||||
|
} |
||||
|
|
||||
|
export default class MemberTable extends React.Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.state = { |
||||
|
members: [], |
||||
|
searchText: '', |
||||
|
filteredMembers: [], |
||||
|
error: undefined, |
||||
|
}; |
||||
|
|
||||
|
this.handleUpdate = this.handleUpdate.bind(this); |
||||
|
this.handleSearch = this.handleSearch.bind(this); |
||||
|
this.clearSearch = this.clearSearch.bind(this); |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
fetch('/api/v1/members/') |
||||
|
.then(checkStatus) |
||||
|
.then(parseJSON) |
||||
|
.then((data) => { |
||||
|
this.setState({ members: data }); |
||||
|
console.log('request succeeded with JSON response', data); |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.log('request failed', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
handleUpdate(event, value) { |
||||
|
this.setState({ ...this.state, searchText: value }); |
||||
|
} |
||||
|
|
||||
|
clearSearch() { |
||||
|
this.setState({ |
||||
|
...this.state, |
||||
|
searchText: '', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
handleSearch() { |
||||
|
const value = this.state.searchText.trim(); |
||||
|
const self = this; |
||||
|
|
||||
|
fetch(`/members/search/${value}/`) |
||||
|
.then((response) => { |
||||
|
if (response.status === 200) { |
||||
|
return response.json(); |
||||
|
} |
||||
|
throw new Error('Bad response from server'); |
||||
|
}) |
||||
|
.then((data) => { |
||||
|
if (data.results.length > 0) { |
||||
|
self.setState({ |
||||
|
...this.state, |
||||
|
error: '', |
||||
|
filteredMembers: this.state.members.filter((member) => { |
||||
|
const ids = data.results.map(m => m.id); |
||||
|
console.log(ids); |
||||
|
|
||||
|
if (ids.indexOf(member.id) !== -1) { |
||||
|
return member; |
||||
|
} |
||||
|
}), |
||||
|
}); |
||||
|
} else { |
||||
|
self.setState({ ...this.state, error: 'Member not found.' }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const memberRows = this.state.members.map(member => ( |
||||
|
<TableRow selectable={false} key={member.id}> |
||||
|
<TableRowColumn>{member.first_name}</TableRowColumn> |
||||
|
<TableRowColumn>{member.last_name}</TableRowColumn> |
||||
|
<TableRowColumn>{member.email}</TableRowColumn> |
||||
|
<TableRowColumn><FlatButton label="Edit" href={`/members/edit/${member.id}`} primary /></TableRowColumn> |
||||
|
</TableRow> |
||||
|
)); |
||||
|
|
||||
|
const filteredMemberRows = this.state.filteredMembers.map(member => ( |
||||
|
<TableRow selectable={false} key={member.id}> |
||||
|
<TableRowColumn>{member.first_name}</TableRowColumn> |
||||
|
<TableRowColumn>{member.last_name}</TableRowColumn> |
||||
|
<TableRowColumn>{member.email}</TableRowColumn> |
||||
|
<TableRowColumn><FlatButton label="Edit" href={`/members/edit/${member.id}`} primary /></TableRowColumn> |
||||
|
</TableRow> |
||||
|
)); |
||||
|
|
||||
|
|
||||
|
return ( |
||||
|
<div className="mdl-grid"> |
||||
|
<div className="mdl-cell mdl-cell--12-col"> |
||||
|
<h3>Members</h3> |
||||
|
<Toolbar> |
||||
|
<ToolbarGroup> |
||||
|
<TextField |
||||
|
hintText="ma@example.com OR name" |
||||
|
floatingLabelText="Search for member" |
||||
|
onChange={this.handleUpdate} |
||||
|
value={this.state.searchText} |
||||
|
/> |
||||
|
<RaisedButton label="Search" primary onClick={this.handleSearch} /> |
||||
|
<RaisedButton label="Clear" onClick={this.clearSearch} secondary /> |
||||
|
</ToolbarGroup> |
||||
|
</Toolbar> |
||||
|
<Table selectable={false}> |
||||
|
<TableHeader adjustForCheckbox={false} displaySelectAll={false}> |
||||
|
<TableRow> |
||||
|
<TableHeaderColumn>First name</TableHeaderColumn> |
||||
|
<TableHeaderColumn>Last Name</TableHeaderColumn> |
||||
|
<TableHeaderColumn>Email</TableHeaderColumn> |
||||
|
<TableHeaderColumn /> |
||||
|
</TableRow> |
||||
|
</TableHeader> |
||||
|
<TableBody displayRowCheckbox={false}> |
||||
|
{filteredMemberRows.length ? |
||||
|
filteredMemberRows : undefined |
||||
|
} |
||||
|
{memberRows.length && !this.state.searchText ? |
||||
|
memberRows : |
||||
|
<TableRow> |
||||
|
<TableRowColumn>{'Members loading.'}</TableRowColumn> |
||||
|
</TableRow> |
||||
|
} |
||||
|
</TableBody> |
||||
|
</Table> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import React from 'react'; |
||||
|
import ReactDOM from 'react-dom'; |
||||
|
import injectTapEventPlugin from 'react-tap-event-plugin'; |
||||
|
import getMuiTheme from 'material-ui/styles/getMuiTheme'; |
||||
|
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; |
||||
|
import MemberTable from './components/MemberTable'; |
||||
|
|
||||
|
// Needed for onTouchTap |
||||
|
// http://stackoverflow.com/a/34015469/988941 |
||||
|
injectTapEventPlugin(); |
||||
|
|
||||
|
|
||||
|
const App = () => ( |
||||
|
<MuiThemeProvider muiTheme={getMuiTheme()}> |
||||
|
<div> |
||||
|
<h1>Members</h1> |
||||
|
<MemberTable /> |
||||
|
</div> |
||||
|
</MuiThemeProvider> |
||||
|
); |
||||
|
|
||||
|
ReactDOM.render(<App />, document.getElementById('root')); |
@ -0,0 +1,13 @@ |
|||||
|
from rest_framework import serializers |
||||
|
from rest_framework.serializers import ModelSerializer |
||||
|
|
||||
|
from .models import Member |
||||
|
|
||||
|
|
||||
|
class MemberSerializer(ModelSerializer): |
||||
|
first_name = serializers.CharField(allow_blank=True, required=False) |
||||
|
last_name = serializers.CharField(allow_blank=True, required=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Member |
||||
|
fields = ('first_name', 'last_name', 'email', 'id') |
@ -1,22 +1,7 @@ |
|||||
{% extends 'dashboard.html' %} |
{% extends 'dashboard.html' %} |
||||
|
{% load render_bundle from webpack_loader %} |
||||
|
|
||||
{% block content %} |
{% block content %} |
||||
<div class="mdl-cell mdl-cell--6-col"> |
<div id="root"></div> |
||||
<ul class="demo-list-three mdl-list"> |
{% render_bundle 'members' %} |
||||
{% for member in members %} |
|
||||
<li class="mdl-list__item mdl-list__item--three-line"> |
|
||||
<span class="mdl-list__item-primary-content"> |
|
||||
<i class="material-icons mdl-list__item-avatar">person</i> |
|
||||
<span>{{ member.full_name }}</span> |
|
||||
<span class="mdl-list__item-text-body"> |
|
||||
{{ member.email }} |
|
||||
</span> |
|
||||
</span> |
|
||||
<span class="mdl-list__item-secondary-content"> |
|
||||
<a class="mdl-list__item-secondary-action" href="{% url 'member_edit' member_id=member.id %}"><i class="material-icons">edit</i></a> |
|
||||
</span> |
|
||||
</li> |
|
||||
{% endfor %} |
|
||||
</ul> |
|
||||
</div> |
|
||||
{% endblock %} |
{% endblock %} |
@ -1,48 +1,50 @@ |
|||||
const path = require("path"); |
const path = require('path'); |
||||
const webpack = require('webpack'); |
const webpack = require('webpack'); |
||||
const BundleTracker = require('webpack-bundle-tracker'); |
const BundleTracker = require('webpack-bundle-tracker'); |
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
||||
const autoprefixer = require('autoprefixer'); |
const autoprefixer = require('autoprefixer'); |
||||
|
|
||||
module.exports = { |
module.exports = { |
||||
context: __dirname, |
context: __dirname, |
||||
devtool: 'inline-source-map', |
devtool: 'inline-source-map', |
||||
entry: './assets/js/index', // entry point of our app. assets/js/index.js should require other js modules and dependencies it needs
|
entry: { |
||||
|
signin: './assets/js/index', |
||||
|
members: './assets/js/members/index', |
||||
|
}, |
||||
|
output: { |
||||
|
path: path.resolve('./assets/bundles/'), |
||||
|
filename: '[name]-[hash].js', |
||||
|
}, |
||||
|
|
||||
output: { |
plugins: [ |
||||
path: path.resolve('./assets/bundles/'), |
new BundleTracker({ filename: './webpack-stats.json' }), |
||||
filename: "[name]-[hash].js" |
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), |
||||
}, |
new webpack.NoErrorsPlugin(), |
||||
|
], |
||||
|
|
||||
plugins: [ |
module: { |
||||
new BundleTracker({filename: './webpack-stats.json'}), |
loaders: [ |
||||
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), |
{ |
||||
new webpack.NoErrorsPlugin() |
test: /\.jsx?$/, |
||||
|
exclude: /node_modules/, |
||||
|
loader: 'babel-loader', |
||||
|
query: { |
||||
|
presets: ['es2015', 'stage-0', 'react'], |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
test: /(\.scss|\.css)$/, |
||||
|
loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox'), |
||||
|
}, |
||||
], |
], |
||||
|
}, |
||||
module: { |
resolve: { |
||||
loaders: [ |
modulesDirectories: [ |
||||
{ |
'node_modules', |
||||
test: /\.jsx?$/, |
'bower_components', |
||||
exclude: /node_modules/, |
path.resolve(__dirname, './node_modules'), |
||||
loader: 'babel-loader', |
], |
||||
query: { |
extensions: ['', '.js', '.jsx', '.scss'], |
||||
presets: ['es2015', 'stage-0', 'react'] |
}, |
||||
} |
postcss: [autoprefixer], |
||||
}, |
}; |
||||
{ |
|
||||
test: /(\.scss|\.css)$/, |
|
||||
loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') |
|
||||
} |
|
||||
] |
|
||||
}, |
|
||||
resolve: { |
|
||||
modulesDirectories: [ |
|
||||
'node_modules', |
|
||||
'bower_components', |
|
||||
path.resolve(__dirname, './node_modules') |
|
||||
], |
|
||||
extensions: ['', '.js', '.jsx', '.scss'] |
|
||||
}, |
|
||||
postcss: [autoprefixer] |
|
||||
} |
|
||||
|
Loading…
Reference in new issue