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' %} |
|||
{% load render_bundle from webpack_loader %} |
|||
|
|||
{% block content %} |
|||
<div class="mdl-cell mdl-cell--6-col"> |
|||
<ul class="demo-list-three mdl-list"> |
|||
{% 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> |
|||
<div id="root"></div> |
|||
{% render_bundle 'members' %} |
|||
{% endblock %} |
@ -1,48 +1,50 @@ |
|||
const path = require("path"); |
|||
const path = require('path'); |
|||
const webpack = require('webpack'); |
|||
const BundleTracker = require('webpack-bundle-tracker'); |
|||
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
|||
const autoprefixer = require('autoprefixer'); |
|||
const autoprefixer = require('autoprefixer'); |
|||
|
|||
module.exports = { |
|||
context: __dirname, |
|||
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
|
|||
context: __dirname, |
|||
devtool: 'inline-source-map', |
|||
entry: { |
|||
signin: './assets/js/index', |
|||
members: './assets/js/members/index', |
|||
}, |
|||
output: { |
|||
path: path.resolve('./assets/bundles/'), |
|||
filename: '[name]-[hash].js', |
|||
}, |
|||
|
|||
output: { |
|||
path: path.resolve('./assets/bundles/'), |
|||
filename: "[name]-[hash].js" |
|||
}, |
|||
plugins: [ |
|||
new BundleTracker({ filename: './webpack-stats.json' }), |
|||
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), |
|||
new webpack.NoErrorsPlugin(), |
|||
], |
|||
|
|||
plugins: [ |
|||
new BundleTracker({filename: './webpack-stats.json'}), |
|||
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), |
|||
new webpack.NoErrorsPlugin() |
|||
module: { |
|||
loaders: [ |
|||
{ |
|||
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: { |
|||
loaders: [ |
|||
{ |
|||
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') |
|||
} |
|||
] |
|||
}, |
|||
resolve: { |
|||
modulesDirectories: [ |
|||
'node_modules', |
|||
'bower_components', |
|||
path.resolve(__dirname, './node_modules') |
|||
], |
|||
extensions: ['', '.js', '.jsx', '.scss'] |
|||
}, |
|||
postcss: [autoprefixer] |
|||
} |
|||
}, |
|||
resolve: { |
|||
modulesDirectories: [ |
|||
'node_modules', |
|||
'bower_components', |
|||
path.resolve(__dirname, './node_modules'), |
|||
], |
|||
extensions: ['', '.js', '.jsx', '.scss'], |
|||
}, |
|||
postcss: [autoprefixer], |
|||
}; |
|||
|
Loading…
Reference in new issue