merge table branch

This commit is contained in:
ddejongh 2015-05-29 09:20:01 +02:00
commit b8ffac4275
21 changed files with 394 additions and 99 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ node_modules
npm-debug.log
build/app.js
.DS_Store

View File

@ -18,9 +18,7 @@ Install some nice extensions for Chrom(e|ium):
git clone git@bitbucket.org:ascribe/onion.git
cd onion
npm install
npm run watch
python -mSimpleHTTPServer
gulp serve
```

View File

@ -12,11 +12,13 @@
.ascribe-table-header-row {
border-bottom: 2px solid rgba(2, 182, 163, 0.5);
border-top: 2px solid rgba(2, 182, 163, 0.5);
padding: 0;
}
.ascribe-table-header-column {
display: table;
height: 4em;
height:3em;
padding: 0;
}
.ascribe-table-header-column > span {
@ -31,10 +33,10 @@
.ascribe-table-header-column > span > .glyphicon {
font-size: .5em;
}
/*
.ascribe-table-item:nth-child(even) {
background-color: #F5F5F5;
}
}*/
/*.ascribe-table-item:hover {
background-color: #EEEEEE;
@ -44,7 +46,7 @@
display: table;
font-family: 'Source Sans Pro';
font-size: 1.2em;
height: 4em;
height:3em;
}
.ascribe-table-item-column > * {
@ -52,6 +54,7 @@
vertical-align: middle;
}
.ascribe-table-item-selected {
/*.btn-ascribe, .btn-ascribe:hover, .btn-ascribe:active, .btn-ascribe:focus {*/
/*background-color: rgba(2, 182, 163, 0.5);*/
/*border-color: rgba(2, 182, 163, 0.5);*/
@ -109,6 +112,19 @@
background-color: rgba(2, 182, 163, 0.5);
}
.ascribe-table-item-selectable {
cursor: default;
}
.piece-list-toolbar {
height:3em;
}
.no-margin {
margin-right: 0;
margin-left: 0;
}
.ascribe-detail-header {
margin-top: 2em;
}

67
gulpfile.js Normal file
View File

@ -0,0 +1,67 @@
var gulp = require('gulp');
var gulpif = require('gulp-if');
var sourcemaps = require('gulp-sourcemaps');
var util = require('gulp-util');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var watchify = require('watchify');
var browserify = require('browserify');
var browserSync = require('browser-sync');
var babelify = require('babelify');
var notify = require('gulp-notify');
var _ = require('lodash');
gulp.task('build', function() {
bundle(false);
});
gulp.task('serve', ['browser-sync'], function() {
bundle(true);
});
gulp.task('browser-sync', function() {
browserSync({
server: {
baseDir: "."
},
port: process.env.PORT || 3000
});
});
function bundle(watch) {
var bro;
if (watch) {
bro = watchify(browserify('./js/app.js',
// Assigning debug to have sourcemaps
_.assign(watchify.args, {
debug: true
})));
bro.on('update', function() {
rebundle(bro, true);
});
} else {
bro = browserify('./js/app.js', {
debug: true
});
}
bro.transform(babelify.configure({
compact: false
}));
function rebundle(bundler, watch) {
return bundler.bundle()
.on('error', notify.onError('Error: <%= error.message %>'))
.pipe(source('app.js'))
.pipe(buffer())
.pipe(sourcemaps.init({
loadMaps: true
})) // loads map from browserify file
.pipe(sourcemaps.write()) // writes .map file
.pipe(gulp.dest('./build'))
.pipe(browserSync.stream());
}
return rebundle(bro);
}

View File

@ -5,7 +5,8 @@ import EditionListFetcher from '../fetchers/edition_list_fetcher.js';
class EditionListActions {
constructor() {
this.generateActions(
'updateEditionList'
'updateEditionList',
'selectEdition'
);
}
@ -14,7 +15,7 @@ class EditionListActions {
.fetch(pieceId)
.then((res) => {
this.actions.updateEditionList({
'editionList': res.editions,
'editionListOfPiece': res.editions,
pieceId
});
})

View File

@ -0,0 +1,32 @@
import React from 'react';
import AppConstants from '../constants/application_constants';
let AclButton = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.array.isRequired
},
render() {
let shouldDisplay = this.props.availableAcls.indexOf(this.props.action) > -1;
let styles = {};
if(shouldDisplay) {
styles.display = 'inline-block';
} else {
styles.display = 'none';
}
return (
<button
style={styles}
type="button"
className="btn btn-default btn-sm">
{this.props.action.toUpperCase()}
</button>
);
}
});
export default AclButton;

View File

@ -0,0 +1,82 @@
import React from 'react';
import EditionListStore from '../../stores/edition_list_store';
import AclButton from '../acl_button';
let PieceListToolbar = React.createClass({
getInitialState() {
return EditionListStore.getState();
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
EditionListStore.listen(this.onChange)
},
componentDidUnmount() {
EditionListStore.unlisten(this.onChange)
},
filterForSelected(edition) {
return edition.selected;
},
fetchSelectedEditionList() {
let selectedEditionList = [];
Object
.keys(this.state.editionList)
.forEach((key) => {
let filteredEditionsForPiece = this.state.editionList[key].filter(this.filterForSelected);
selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece);
});
return selectedEditionList;
},
intersectAcls(a, b) {
return a.filter((val) => b.indexOf(val) > -1);
},
getAvailableAcls() {
let availableAcls = [];
let selectedEditionList = this.fetchSelectedEditionList();
// If no edition has been selected, availableActions is empty
// If only one edition has been selected, their actions are available
// If more than one editions have been selected, their acl properties are intersected
if(selectedEditionList.length >= 1) {
availableAcls = selectedEditionList[0].acl;
}
if(selectedEditionList.length >= 2) {
for(let i = 1; i < selectedEditionList.length; i++) {
availableAcls = this.intersectAcls(availableAcls, selectedEditionList[i].acl);
}
}
return availableAcls;
},
render() {
let availableAcls = this.getAvailableAcls();
return (
<div className="row no-margin">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 piece-list-toolbar">
<div className="pull-right">
<AclButton availableAcls={availableAcls} action="transfer" />
<AclButton availableAcls={availableAcls} action="consign" />
<AclButton availableAcls={availableAcls} action="share" />
<AclButton availableAcls={availableAcls} action="loan" />
</div>
</div>
</div>
);
}
});
export default PieceListToolbar;

View File

@ -27,7 +27,13 @@ let Table = React.createClass({
if(this.props.itemList && this.props.itemList.length > 0) {
return (
<div className="ascribe-table">
<TableHeader columnList={this.props.columnList} itemList={this.props.itemList} fetchList={this.props.fetchList} changeOrder={this.props.changeOrder} orderAsc={this.props.orderAsc} orderBy={this.props.orderBy} />
<TableHeader
columnList={this.props.columnList}
itemList={this.props.itemList}
fetchList={this.props.fetchList}
changeOrder={this.props.changeOrder}
orderAsc={this.props.orderAsc}
orderBy={this.props.orderBy} />
{this.renderChildren()}
</div>
);

View File

@ -12,9 +12,9 @@ let TableHeader = React.createClass({
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
itemList: React.PropTypes.array.isRequired,
changeOrder: React.PropTypes.func.isRequired,
orderAsc: React.PropTypes.bool.isRequired,
orderBy: React.PropTypes.string.isRequired
changeOrder: React.PropTypes.func,
orderAsc: React.PropTypes.bool,
orderBy: React.PropTypes.string
},
render() {
@ -23,7 +23,7 @@ let TableHeader = React.createClass({
<div className="row">
{this.props.columnList.map((val, i) => {
let columnClasses = this.calcColumnClasses(this.props.columnList, i);
let columnClasses = this.calcColumnClasses(this.props.columnList, i, 12);
let columnName = this.props.columnList[i].columnName;
let canBeOrdered = this.props.columnList[i].canBeOrdered;

View File

@ -8,10 +8,10 @@ let TableHeaderItem = React.createClass({
columnClasses: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string.isRequired,
columnName: React.PropTypes.string.isRequired,
canBeOrdered: React.PropTypes.bool.isRequired,
changeOrder: React.PropTypes.func.isRequired,
orderAsc: React.PropTypes.bool.isRequired,
orderBy: React.PropTypes.string.isRequired
canBeOrdered: React.PropTypes.bool,
changeOrder: React.PropTypes.func,
orderAsc: React.PropTypes.bool,
orderBy: React.PropTypes.string
},
changeOrder() {
@ -19,7 +19,7 @@ let TableHeaderItem = React.createClass({
},
render() {
if(this.props.canBeOrdered) {
if(this.props.canBeOrdered && this.props.changeOrder && this.props.orderAsc != null && this.props.orderBy) {
if(this.props.columnName === this.props.orderBy) {
return (
<div

View File

@ -1,38 +1,30 @@
import React from 'react';
import TableColumnMixin from '../../mixins/table_column_mixin';
import TableColumnContentModel from '../../models/table_column_content_model';
import TableItemWrapper from './table_item_wrapper';
let TableItem = React.createClass({
mixins: [TableColumnMixin],
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object
columnContent: React.PropTypes.object,
onClick: React.PropTypes.func, // See: https://facebook.github.io/react/tips/expose-component-functions.html
classNames: React.PropTypes.string
},
render() {
let calcColumnElementContent = () => {
return this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
});
};
return (
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item">
<div
className={this.props.classNames + ' col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item'}
onClick={this.props.onClick}>
<div className="row">
{calcColumnElementContent()}
<TableItemWrapper
columnList={this.props.columnList}
columnContent={this.props.columnContent}
columnWidth={12}>
</TableItemWrapper>
</div>
</div>
);

View File

@ -0,0 +1,18 @@
import React from 'react';
let TableItemAcl = React.createClass({
propTypes: {
content: React.PropTypes.array.isRequired
},
render() {
return (
<span>
{this.props.content.join('/')}
</span>
);
}
});
export default TableItemAcl;

View File

@ -0,0 +1,38 @@
import React from 'react';
import classNames from 'classnames';
import TableColumnContentModel from '../../models/table_column_content_model';
import TableItem from './table_item';
// This component is implemented as recommended here: http://stackoverflow.com/a/25723635/1263876
let TableItemSelectable = React.createClass({
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object,
parentId: React.PropTypes.number,
className: React.PropTypes.string
},
selectItem() {
this.props.selectItem(this.props.parentId, this.props.columnContent.edition_number);
},
render() {
let tableItemClasses = classNames({
'ascribe-table-item-selected': this.props.columnContent.selected
});
return (
<TableItem
classNames={tableItemClasses + ' ' + this.props.className}
columnList={this.props.columnList}
columnContent={this.props.columnContent}
onClick={this.selectItem}>
</TableItem>
);
}
});
export default TableItemSelectable;

View File

@ -5,12 +5,12 @@ import TableColumnContentModel from '../../models/table_column_content_model';
import EditionListStore from '../../stores/edition_list_store';
import EditionListActions from '../../actions/edition_list_actions';
// ToDo: Create Table-specific Utils to not lock it to projects utilities
import GeneralUtils from '../../utils/general_utils';
import Table from './table';
import TableItem from './table_item';
import TableItemWrapper from './table_item_wrapper';
import TableItemText from './table_item_text';
import TableItemAcl from './table_item_acl';
import TableItemSelectable from './table_item_selectable';
import TableItemSubtableButton from './table_item_subtable_button';
@ -40,6 +40,7 @@ let TableItemSubtable = React.createClass({
'open': false
});
} else {
EditionListActions.fetchEditionList(this.props.columnContent.id);
this.setState({
'open': true,
@ -48,43 +49,21 @@ let TableItemSubtable = React.createClass({
}
},
calcColumnClasses(list, i) {
let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-'];
let listOfRowValues = list.map((column) => column.rowWidth );
let numOfColumns = GeneralUtils.sumNumList(listOfRowValues);
if(numOfColumns > 10) {
throw new Error('Bootstrap has only 12 columns to assign. You defined ' + numOfColumns + '. Change this in the columnMap you\'re passing to the table.')
} else {
return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i];
}
selectItem(parentId, itemId) {
EditionListActions.selectEdition({
'pieceId': parentId,
'editionId': itemId
});
},
render() {
let calcColumnElementContent = () => {
return this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
});
};
let renderEditionListTable = () => {
let columnList = [
new TableColumnContentModel('edition_number', 'Edition Number', TableItemText, 2, false),
new TableColumnContentModel('user_registered', 'User', TableItemText, 4, true),
new TableColumnContentModel('bitcoin_id', 'Bitcoin Address', TableItemText, 4, true)
new TableColumnContentModel('acl', 'Actions', TableItemAcl, 4, true)
];
if(this.state.open && this.state.editionList[this.props.columnContent.id] && this.state.editionList[this.props.columnContent.id].length) {
@ -94,9 +73,12 @@ let TableItemSubtable = React.createClass({
<Table itemList={this.state.editionList[this.props.columnContent.id]} columnList={columnList}>
{this.state.editionList[this.props.columnContent.id].map((edition, i) => {
return (
<TableItem
<TableItemSelectable
className="ascribe-table-item-selectable"
selectItem={this.selectItem}
parentId={this.props.columnContent.id}
key={i}>
</TableItem>
</TableItemSelectable>
);
})}
</Table>
@ -109,9 +91,13 @@ let TableItemSubtable = React.createClass({
return (
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item">
<div className="row">
{calcColumnElementContent()}
<div className="col-xs-2 col-sm-2 col-md-2 col-lg-2 ascribe-table-item-column">
<TableItemSubtableButton content="Editions" onClick={this.loadEditionList}>
<TableItemWrapper
columnList={this.props.columnList}
columnContent={this.props.columnContent}
columnWidth={12}>
</TableItemWrapper>
<div className="col-xs-1 col-sm-1 col-md-1 col-lg-1 ascribe-table-item-column">
<TableItemSubtableButton content="+" onClick={this.loadEditionList}>
</TableItemSubtableButton>
</div>
</div>

View File

@ -10,7 +10,7 @@ let TableItemSubtableButton = React.createClass({
render() {
return (
<span>
<button type="button" className="btn btn-ascribe btn-primary btn-sm" onClick={this.props.onClick}>
<button type="button" className="btn btn-default btn-sm ascribe-table-expand-button" onClick={this.props.onClick}>
{this.props.content}
</button>
</span>

View File

@ -0,0 +1,34 @@
import React from 'react';
import TableColumnContentModel from '../../models/table_column_content_model';
import TableColumnMixin from '../../mixins/table_column_mixin';
let TableItemWrapper = React.createClass({
mixins: [TableColumnMixin],
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object,
columnWidth: React.PropTypes.number.isRequired
},
render() {
return (
<div>
{this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i, this.props.columnWidth);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
})}
</div>
);
}
});
export default TableItemWrapper;

View File

@ -15,6 +15,8 @@ import TableColumnContentModel from '../models/table_column_content_model';
import Pagination from './ascribe_pagination/pagination';
import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar';
let PieceList = React.createClass({
@ -62,6 +64,7 @@ let PieceList = React.createClass({
if(this.state.pieceList && this.state.pieceList.length > 0) {
return (
<div>
<PieceListToolbar />
<Table
columnList={columnList}
changeOrder={this.tableChangeOrder}

View File

@ -1,8 +1,7 @@
let constants = {
'baseUrl': 'http://localhost:8000/api/',
//'baseUrl': 'http://staging.ascribe.io/api/',
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw' // dimi@mailinator:0000000000
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw', // dimi@mailinator:0000000000
'aclList': ['edit', 'consign', 'transfer', 'loan', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection']
};
export default constants;
export default constants;

View File

@ -7,14 +7,14 @@ let TableColumnMixin = {
* Generates the bootstrap grid column declarations automatically using
* the columnMap.
*/
calcColumnClasses(list, i) {
calcColumnClasses(list, i, numOfColumns) {
let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-'];
let listOfRowValues = list.map((column) => column.rowWidth );
let numOfColumns = GeneralUtils.sumNumList(listOfRowValues);
let numOfUsedColumns = GeneralUtils.sumNumList(listOfRowValues);
if(numOfColumns > 12) {
throw new Error('Bootstrap has only 12 columns to assign. You defined ' + numOfColumns + '. Change this in the columnMap you\'re passing to the table.')
if(numOfUsedColumns > numOfColumns) {
throw new Error('This table has only ' + numOfColumns + ' columns to assign. You defined ' + numOfUsedColumns + '. Change this in the columnMap you\'re passing to the table.')
} else {
return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i];
}

View File

@ -1,3 +1,5 @@
import React from 'react';
import alt from '../alt';
import EditionsListActions from '../actions/edition_list_actions';
@ -7,8 +9,29 @@ class EditionListStore {
this.bindActions(EditionsListActions);
}
onUpdateEditionList({pieceId, editionList}) {
this.editionList[pieceId] = editionList;
onUpdateEditionList({pieceId, editionListOfPiece}) {
if(this.editionList[pieceId]) {
this.editionList[pieceId].forEach((edition, i) => {
// This uses the index of the new editionList for determining the edition.
// If the list of editions can be sorted in the future, this needs to be changed!
editionListOfPiece[i] = React.addons.update(edition, {$merge: editionListOfPiece[i]});
})
}
this.editionList[pieceId] = editionListOfPiece;
}
onSelectEdition({pieceId, editionId}) {
this.editionList[pieceId].forEach((edition) => {
if(edition.edition_number === editionId) {
if(edition.selected) {
edition.selected = false;
} else {
edition.selected = true;
}
}
});
}
};

View File

@ -3,26 +3,24 @@
"version": "0.0.1",
"description": "Das neue web client for Ascribe",
"main": "js/app.js",
"scripts": {
"watch": "watchify -o build/app.js -v -d js/app.js",
"build": "browserify . -t [envify --NODE_ENV production] | uglifyjs -cm > build/app.js",
"test": "jest"
},
"author": "Ascribe",
"license": "Copyright",
"browserify": {
"transform": [
"babelify",
"envify"
]
},
"devDependencies": {
"babel-jest": "^4.0.0",
"babelify": "^6.0.2",
"babelify": "^6.1.2",
"browser-sync": "^2.7.5",
"browserify": "^9.0.8",
"envify": "^3.4.0",
"gulp": "^3.8.11",
"gulp-if": "^1.2.5",
"gulp-notify": "^2.2.0",
"gulp-sourcemaps": "^1.5.2",
"gulp-util": "^3.0.4",
"jest-cli": "^0.4.0",
"lodash": "^3.9.3",
"reactify": "^1.1.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.1.2"
},
"dependencies": {