diff --git a/build.sh b/build.sh index 653c7eb6..ee59cac7 100755 --- a/build.sh +++ b/build.sh @@ -11,13 +11,10 @@ then exit 1 fi -# create new version -npm version "$1" # build npm install cd client gulp build cd .. # push -docker build -t "$DEFAULT_DOCKER_REGISTRY/stups/yourturn:$1" . && docker push "$DEFAULT_DOCKER_REGISTRY/stups/yourturn:$1" -git push --tags \ No newline at end of file +docker build -t "$DEFAULT_DOCKER_REGISTRY/stups/yourturn:$1" . && docker push "$DEFAULT_DOCKER_REGISTRY/stups/yourturn:$1" \ No newline at end of file diff --git a/client/Gulpfile.js b/client/Gulpfile.js index 59587985..a46a6a99 100644 --- a/client/Gulpfile.js +++ b/client/Gulpfile.js @@ -26,6 +26,7 @@ var LODASH_FUNCS = [ 'groupBy', 'intersection', 'isEmpty', + 'partition', 'pluck', 'reverse', 'sortBy', diff --git a/client/lib/common/asset/less/common/account-selector.less b/client/lib/common/asset/less/common/account-selector.less index dbaba35c..aa48a4f2 100644 --- a/client/lib/common/asset/less/common/account-selector.less +++ b/client/lib/common/asset/less/common/account-selector.less @@ -3,6 +3,11 @@ .account-selector { padding: @padding-small 0; + .account-selector-list { + max-height: 300px; + overflow: auto; + } + label { .transition(background, color); cursor: pointer; @@ -50,9 +55,13 @@ } } + .account-selector-toggle-buttonsĀ { + padding: @padding-tiniest 0; + } + .input-group { display: flex; - padding-bottom: @padding-small; + width: 30em; .fa { user-select: none; flex: 0 0 auto; @@ -69,6 +78,7 @@ input { border-top-left-radius: 0px; border-bottom-left-radius: 0px; + width: 100%; } } } \ No newline at end of file diff --git a/client/lib/common/src/lodash.custom.js b/client/lib/common/src/lodash.custom.js index bf9156bd..163efaf8 100644 --- a/client/lib/common/src/lodash.custom.js +++ b/client/lib/common/src/lodash.custom.js @@ -1,7 +1,7 @@ /** * @license * lodash 3.9.3 (Custom Build) - * Build: `lodash modern include="chain,debounce,difference,extend,filter,findLastIndex,flatten,forOwn,groupBy,intersection,isEmpty,pluck,reverse,sortBy,slice,take,times,value,values" -d -o lib/common/src/lodash.custom.js` + * Build: `lodash modern include="chain,debounce,difference,extend,filter,findLastIndex,flatten,forOwn,groupBy,intersection,isEmpty,partition,pluck,reverse,sortBy,slice,take,times,value,values" -d -o lib/common/src/lodash.custom.js` * Copyright 2012-2015 The Dojo Foundation * Based on Underscore.js 1.8.3 * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors @@ -3354,6 +3354,69 @@ return func(collection, iteratee); } + /** + * Creates an array of elements split into two groups, the first of which + * contains elements `predicate` returns truthy for, while the second of which + * contains elements `predicate` returns falsey for. The predicate is bound + * to `thisArg` and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the array of grouped elements. + * @example + * + * _.partition([1, 2, 3], function(n) { + * return n % 2; + * }); + * // => [[1, 3], [2]] + * + * _.partition([1.2, 2.3, 3.4], function(n) { + * return this.floor(n) % 2; + * }, Math); + * // => [[1.2, 3.4], [2.3]] + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true }, + * { 'user': 'pebbles', 'age': 1, 'active': false } + * ]; + * + * var mapper = function(array) { + * return _.pluck(array, 'user'); + * }; + * + * // using the `_.matches` callback shorthand + * _.map(_.partition(users, { 'age': 1, 'active': false }), mapper); + * // => [['pebbles'], ['barney', 'fred']] + * + * // using the `_.matchesProperty` callback shorthand + * _.map(_.partition(users, 'active', false), mapper); + * // => [['barney', 'pebbles'], ['fred']] + * + * // using the `_.property` callback shorthand + * _.map(_.partition(users, 'active'), mapper); + * // => [['fred'], ['barney', 'pebbles']] + */ + var partition = createAggregator(function(result, value, key) { + result[key ? 0 : 1].push(value); + }, function() { return [[], []]; }); + /** * Gets the property value of `path` from all elements in `collection`. * @@ -4461,6 +4524,7 @@ lodash.matches = matches; lodash.mixin = mixin; lodash.pairs = pairs; + lodash.partition = partition; lodash.pluck = pluck; lodash.property = property; lodash.restParam = restParam; diff --git a/client/lib/violation/src/account-selector.jsx b/client/lib/violation/src/account-selector.jsx index 2bf3215e..633110ee 100644 --- a/client/lib/violation/src/account-selector.jsx +++ b/client/lib/violation/src/account-selector.jsx @@ -1,40 +1,50 @@ import React from 'react'; import Icon from 'react-fa'; +import _ from 'lodash'; import {Typeahead} from 'react-typeahead'; +import fuzzysearch from 'fuzzysearch'; import 'common/asset/less/common/account-selector.less'; function filterOptionFn(input, option) { return input .trim() .split(' ') - .some(term => (option.name + option.id).indexOf(term) >= 0); + .some(term => fuzzysearch(term, option.name + option.id)); +} + +function getDisplayedAccounts(selected, filter) { + return filter ? + selected.filter(a => fuzzysearch(filter, a.name)) : + selected; } class AccountSelector extends React.Component { constructor(props) { super(); this.state = { + allSelected: props.selectedAccounts ? props.selectedAccounts.length === props.selectableAccounts.length : false, + filter: '', selectedAccounts: props.selectedAccounts || [] }; } + _selectAll() { + this.props.onToggleAccount(this.props.selectableAccounts.map(a => a.id)); + + this.setState({ + allSelected: true, + selectedAccounts: _.sortBy(this.props.selectableAccounts, 'name') + }); + } + _selectAccount(account) { let {id} = account; if (this.state.selectedAccounts.map(a => a.id).indexOf(id) >= 0) { return; } this.state.selectedAccounts.push(account); - this.state.selectedAccounts - .sort((a, b) => { - let aName = a.name.toLowerCase(), - bName = b.name.toLowerCase(); - return aName < bName ? - -1 : - bName < aName ? - 1 : 0; - }); this.setState({ - selectedAccounts: this.state.selectedAccounts + selectedAccounts: _.sortBy(this.state.selectedAccounts, 'name') }); let {activeAccountIds} = this.props; if (activeAccountIds.indexOf(account.id) < 0) { @@ -56,23 +66,75 @@ class AccountSelector extends React.Component { } } + _toggleAll() { + let displayedIds = getDisplayedAccounts(this.state.selectedAccounts, this.state.filter).map(a => a.id), + activeIds = this.props.activeAccountIds.concat(displayedIds); + // dedup + activeIds = activeIds.filter((el, idx, all) => all.lastIndexOf(el) === idx); + this.props.onToggleAccount(activeIds); + } + + _untoggleAll() { + let displayedIds = getDisplayedAccounts(this.state.selectedAccounts, this.state.filter).map(a => a.id), + activeIds = this.props.activeAccountIds.filter(a => displayedIds.indexOf(a) < 0); + this.props.onToggleAccount(activeIds); + } + + _filter(evt) { + this.setState({ + filter: evt.target.value + }); + } + render() { let {selectableAccounts, activeAccountIds} = this.props, - {selectedAccounts} = this.state; + {selectedAccounts} = this.state, + displayedAccounts = getDisplayedAccounts(selectedAccounts, this.state.filter), + partitionedAccounts = _.partition(displayedAccounts, a => activeAccountIds.indexOf(a.id) >= 0), + activeAccounts = partitionedAccounts[0], + inactiveAccounts = partitionedAccounts[1]; return
Show violations in accounts:
- You can search by name or account number. -
- - `${option.name} (${option.id})`} - filterOption={filterOptionFn} - onOptionSelected={this._selectAccount.bind(this)} - maxVisible={10} /> + {!this.state.allSelected ? +
+
+ You can search by name or account number or + Select all accounts + + +
+
+ + `${option.name} (${option.id})`} + filterOption={filterOptionFn} + onOptionSelected={this._selectAccount.bind(this)} + maxVisible={10} /> +
+
+ : +
+ + +
} +
+
+ Toggle all +
+
+ Untoggle all +
- {selectedAccounts.map(a => +
+ {_.sortBy(activeAccounts, 'name') + .map(a => )} + {_.sortBy(inactiveAccounts, 'name') + .map(a => + )} +
; } } diff --git a/client/lib/violation/src/violation-analysis/violation-analysis.jsx b/client/lib/violation/src/violation-analysis/violation-analysis.jsx index 6fb814ca..e5bbc218 100644 --- a/client/lib/violation/src/violation-analysis/violation-analysis.jsx +++ b/client/lib/violation/src/violation-analysis/violation-analysis.jsx @@ -120,7 +120,7 @@ class ViolationAnalysis extends React.Component { return
No violations! -
+
; } } ViolationAnalysis.displayName = 'ViolationAnalysis';