Skip to content

Commit

Permalink
Merge pull request #73 from edu-xored/WEBUI-66
Browse files Browse the repository at this point in the history
WIP: Admin Users/Teams pages (issue #66)
  • Loading branch information
sergeyt authored Nov 28, 2016
2 parents e920f11 + d5b52ed commit 7aac922
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 54 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@types/query-string": "^3.0.30",
"@types/react": "^0.14.50",
"@types/react-dom": "^0.14.19",
"@types/react-modal": "^1.3.6",
"@types/react-router": "^2.0.39",
"@types/sequelize": "^4.0.39",
"@types/whatwg-fetch": "0.0.32",
Expand All @@ -66,6 +67,7 @@
"query-string": "^4.2.3",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-modal": "^1.5.2",
"react-router": "^3.0.0",
"semantic-ui-react": "^0.61.5",
"sequelize": "^3.27.0",
Expand Down
9 changes: 7 additions & 2 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function makeHeaders() {
const token = getToken();
return {
Authorization: token ? 'Bearer ' + token : undefined,
'Content-Type': CONTENT_JSON,
'Content-Type': CONTENT_JSON
};
}

Expand Down Expand Up @@ -80,7 +80,12 @@ function makeAPI<T, E>(api, ext?: E) {
credentials: "same-origin",
method: 'DELETE',
headers: makeHeaders(),
}).then(toJSON);
}).then(res => {
if (res.status === 200) {
return true;
}
throw new Error(`http error: ${res.statusText}`);
});
},
}, ext);
}
Expand Down
75 changes: 75 additions & 0 deletions src/client/components/admin/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as React from 'react';
import * as ReactModal from 'react-modal';

import { Icon } from 'semantic-ui-react';

const customStyles = {
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
};

interface IModalState {
entity: any;
};

interface IModalProps {
entity: any;
closeModal: any;
fields: string[];
modalIsOpen: boolean;
action: (entity: any) => void;
}

export default class Modal extends React.Component<IModalProps, IModalState> {
constructor(props) {
super(props);
this.state = { entity: this.props.entity };
}

componentWillReceiveProps(nextProps: IModalProps) {
this.setState({ entity: nextProps.entity });
}

apply = () => {
this.props.action(this.state.entity);
this.props.closeModal();
}

renderEditField(fieldName: string) {
const handleOnChange = (e: any) => {
let state = this.state;
this.state.entity[fieldName] = e.target.value;
this.setState(state);
};
return (
<div id={`edit-field-${fieldName}`} key={fieldName} >
<label>{fieldName}:</label>
<input placeholder={fieldName} value={this.state.entity[fieldName]} onChange={handleOnChange} />
</div>
);
}

render() {
return (
<ReactModal
isOpen={this.props.modalIsOpen}
onRequestClose={this.props.closeModal}
style={customStyles}
>
{this.props.fields.map((fieldName) => this.renderEditField(fieldName))}
<button onClick={this.props.closeModal}>
<Icon name='cancel' />
</button>
<button onClick={this.apply}>
<Icon name='save' />
</button>
</ReactModal>
);
}
}
138 changes: 138 additions & 0 deletions src/client/components/admin/TableView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as _ from 'lodash';
import * as React from 'react';
import { Table, Container, Icon } from 'semantic-ui-react';

import Modal from './Modal';

interface ITableViewState {
data: any[];
modalIsOpen: boolean;
modalData: any;
modalAction: (d: any) => void;
};

interface ITableViewProps {
defaultModalData: any;
headers: string[];
modalFields: string[];
api: any;
}

export default class TableView extends React.Component<ITableViewProps, ITableViewState> {
defaultState = {
data: [],
modalIsOpen: false,
modalData: this.props.defaultModalData,
modalAction: null
};

constructor(props) {
super(props);
this.state = this.defaultState;
this.init();
}

init = () => {
this.props.api.getList().then((entities: any[]) => {
this.setState(Object.assign({}, this.state, { data: entities }));
});
}

handlePromise = (p: Promise<any>) => {
p.then(() => {
this.init();
}, err => {
alert(err);
});
}

add = (entity: any) => {
this.handlePromise(this.props.api.create(entity));
}

edit = (entity: any) => {
this.handlePromise(this.props.api.update(entity.id, entity));
}

delete = (id: string) => {
this.handlePromise(this.props.api.remove(id));
}

openModal = (id?: string) => {
let state = this.state;
this.state.modalAction = this.add;
this.state.modalData = Object.assign({}, this.props.defaultModalData);
state.modalIsOpen = true;
if (id) {
state.modalData = this.state.data.find(t => t.id === id);
state.modalAction = this.edit;
}
this.setState(state);
}

closeModal = () => {
this.setState(Object.assign({}, this.state, { modalIsOpen: false }));
}

render() {
return (
<div>
<Modal entity={this.state.modalData} fields={this.props.modalFields} action={this.state.modalAction} closeModal={this.closeModal} modalIsOpen={this.state.modalIsOpen} />
<Container>
<Table>
<Table.Header fullWidth={false}>
{
_.map(this.props.headers, header => (
<Table.HeaderCell key={header}>
{header}
</Table.HeaderCell>
))
}
<Table.HeaderCell>
<button onClick={() => this.openModal()}>
<Icon name='add' />
</button>
</Table.HeaderCell>
</Table.Header>
<Table.Body>
{ _.map(this.state.data, (entity) => this.renderRow(entity)) }
</Table.Body>
</Table>
</Container >
</div>
);
}

renderRow(entity: any) {
const onEdit = () => this.openModal(entity.id);
const onDelete = () => this.delete(entity.id);
return (
<Table.Row key={entity.id}>
{ _.map(this.props.headers, prop => this.renderField(entity, prop)) }
<Table.Cell key='buttons'>
<button onClick={onEdit}>
<Icon name='edit' />
</button>
<button onClick={onDelete}>
<Icon name='delete' />
</button>
</Table.Cell>
</Table.Row>
);
}

renderField(entity: any, propertyName: string) {
const value = entity[propertyName];
let content = value;

if (propertyName === "avatar") {
content = <img src={value} />;
}

return (
<Table.Cell key={propertyName}>
{content}
</Table.Cell>
);
}
}
4 changes: 1 addition & 3 deletions src/client/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import * as React from 'react';
import { render } from 'react-dom';
import Routes from './routes';
import PageHeader from './pageheader';
import API from './api';

class Root extends React.Component<{}, {}> {
render() {
return Routes;
}
}

render(<Root/>, document.getElementById('root'));
render(<Root />, document.getElementById('root'));
13 changes: 12 additions & 1 deletion src/client/pageheader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ const menu: MenuItem[] = [
{
name: 'admin',
text: 'Admin',
link: '/admin',
items: [
{
name: 'users',
text: 'Users',
link: '/admin/users'
},
{
name: 'teams',
text: 'Teams',
link: '/admin/teams'
}
]
},
/*
{
Expand Down
26 changes: 26 additions & 0 deletions src/client/pages/admin/teams.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

import { Header, Container } from 'semantic-ui-react';
import TeamsView from '../../components/admin/TableView';
import api from '../../api';

const headers = ['id', 'avatar', 'name', 'description'];
const modalFields = ['name', 'avatar', 'description'];

const defaultModalData: any = {
id: '',
name: '',
avatar: '',
description: ''
};

export default class TeamsPage extends React.Component<{}, {}> {
render() {
return (
<Container>
<Header content='Teams' />
<TeamsView headers={headers} modalFields={modalFields} api={api.teams} defaultModalData={defaultModalData} />
</Container>
);
}
}
26 changes: 26 additions & 0 deletions src/client/pages/admin/users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

import { Header, Container } from 'semantic-ui-react';
import TeamsView from '../../components/admin/TableView';
import api from '../../api';

const headers = ['id', 'avatar', 'name', 'email', 'login', 'role', 'position', 'place'];
const modalFields = ['name', 'avatar', 'email'];

const defaultModalData: any = {
id: '',
name: '',
avatar: '',
email: ''
};

export default class TeamsPage extends React.Component<{}, {}> {
render() {
return (
<Container>
<Header content='Users' />
<TeamsView headers={headers} modalFields={modalFields} api={api.users} defaultModalData={defaultModalData} />
</Container>
);
}
}
Loading

0 comments on commit 7aac922

Please sign in to comment.