Merge pull request #3459 from FinnStutzenstein/MotionOptimizations

Motion optimizations
This commit is contained in:
Emanuel Schütze 2017-11-02 09:10:20 +01:00 committed by GitHub
commit 4a2d09e56c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 196 additions and 61 deletions

View File

@ -45,6 +45,8 @@ Motions:
- Clear identifier on state reset [#3356]. - Clear identifier on state reset [#3356].
- New config options to hide reason and recommendation on projector [#3432]. - New config options to hide reason and recommendation on projector [#3432].
- Show motion identifier in (current) list of speakers [#3442] - Show motion identifier in (current) list of speakers [#3442]
- Added navigation between single motions [#3459].
- Improved the multiselect state filter [#3459].
Elections: Elections:
- Added pagination for list view [#3393]. - Added pagination for list view [#3393].

View File

@ -319,8 +319,10 @@ angular.module('OpenSlidesApp.assignments.site', [
}; };
// Sorting // Sorting
$scope.sort = osTableSort.createInstance(); $scope.sort = osTableSort.createInstance('AssignmentTableSort');
$scope.sort.column = 'title'; if (!$scope.sort.column) {
$scope.sort.column = 'title';
}
$scope.sortOptions = [ $scope.sortOptions = [
{name: 'agenda_item.getItemNumberWithAncestors()', {name: 'agenda_item.getItemNumberWithAncestors()',
display_name: gettext('Item')}, display_name: gettext('Item')},

View File

@ -215,7 +215,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString | osFilter: filter.filterString : filter.getObjectQueryString
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag | MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
| MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase | MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase
| orderBy: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo : itemsPerPage : limitBegin"> | limitTo : itemsPerPage : limitBegin">
<!-- select column --> <!-- select column -->

View File

@ -48,6 +48,14 @@ body {
color: #222; color: #222;
} }
/* override booststrap's label class to fix linebreak and add spacing */
.label {
display: inline-block;
padding: .4em .6em;
margin-right: .2em;
white-space: normal;
}
/*** ProjectorContainer ***/ /*** ProjectorContainer ***/
.pContainer { .pContainer {
background-color: #222; background-color: #222;
@ -573,6 +581,18 @@ p.os-split-after {
display: none; display: none;
} }
/*** Motion blocks ***/
.motion-block {
display: flex;
flex-wrap: wrap;
padding: 10px;
}
.motion-block > div {
width: 50%;
margin-bottom: 15px;
line-height: 1em;
}
/*** Video and Image projection ***/ /*** Video and Image projection ***/
img.projector-image { img.projector-image {
width: 100%; width: 100%;

View File

@ -1420,8 +1420,8 @@ angular.module('OpenSlidesApp.core', [
]) ])
// filters the requesting object (id=selfid) from a list of input objects // filters the requesting object (id=selfid) from a list of input objects
.filter('notself', function() { .filter('notself', function () {
return function(input, selfid) { return function (input, selfid) {
var result; var result;
if (selfid) { if (selfid) {
result = []; result = [];
@ -1438,6 +1438,26 @@ angular.module('OpenSlidesApp.core', [
}; };
}) })
// Wraps the orderBy filter. But puts ("", null, undefined) last.
.filter('orderByEmptyLast', [
'$filter',
function ($filter) {
return function (array, sortPredicate, reverseOrder, compareFn) {
var falsyItems = [];
var truthyItems = _.filter(array, function (item) {
var falsy = item[sortPredicate] === void 0 ||
item[sortPredicate] === null || item[sortPredicate] === '';
if (falsy) {
falsyItems.push(item);
}
return !falsy;
});
truthyItems = $filter('orderBy')(truthyItems, sortPredicate, reverseOrder, compareFn);
return _.concat(truthyItems, falsyItems);
};
}
])
// Make sure that the DS factories are loaded by making them a dependency // Make sure that the DS factories are loaded by making them a dependency
.run([ .run([
'ChatMessage', 'ChatMessage',

View File

@ -573,17 +573,27 @@ angular.module('OpenSlidesApp.core.site', [
* instance.column='title') * instance.column='title')
*/ */
.factory('osTableSort', [ .factory('osTableSort', [
function () { '$sessionStorage',
var createInstance = function () { function ($sessionStorage) {
var createInstance = function (tableName) {
var self = { var self = {
column: '', column: '',
reverse: false, reverse: false,
}; };
var storage = $sessionStorage[tableName];
if (storage) {
self = storage;
}
self.save = function () {
$sessionStorage[tableName] = self;
};
self.toggle = function (column) { self.toggle = function (column) {
if (self.column === column) { if (self.column === column) {
self.reverse = !self.reverse; self.reverse = !self.reverse;
} }
self.column = column; self.column = column;
self.save();
}; };
return self; return self;
}; };

View File

@ -89,8 +89,10 @@ angular.module('OpenSlidesApp.mediafiles.list', [
function (mediafile) {return mediafile.mediafile.name;}, function (mediafile) {return mediafile.mediafile.name;},
]; ];
// Sorting // Sorting
$scope.sort = osTableSort.createInstance(); $scope.sort = osTableSort.createInstance('MediafileTableSort');
$scope.sort.column = 'title_or_filename'; if (!$scope.sort.column) {
$scope.sort.column = 'title_or_filename';
}
$scope.sortOptions = [ $scope.sortOptions = [
{name: 'title_or_filename', {name: 'title_or_filename',
display_name: gettext('Title')}, display_name: gettext('Title')},

View File

@ -240,7 +240,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString | osFilter: filter.filterString : filter.getObjectQueryString
| filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))} | filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))}
| filter: {hidden: filter.booleanFilters.isHidden.value} | filter: {hidden: filter.booleanFilters.isHidden.value}
| orderBy: sort.column : sort.reverse )"> | orderByEmptyLast: sort.column : sort.reverse )">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="mediafiles.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="mediafiles.can_manage" class="col-xs-1 centered">

View File

@ -907,6 +907,7 @@ angular.module('OpenSlidesApp.motions.site', [
motion.star = false; motion.star = false;
} }
}); });
$scope.collectStatesAndRecommendations();
}); });
$scope.alert = {}; $scope.alert = {};
@ -917,40 +918,69 @@ angular.module('OpenSlidesApp.motions.site', [
}; };
// collect all states and all recommendations of all workflows // collect all states and all recommendations of all workflows
$scope.states = []; $scope.collectStatesAndRecommendations = function () {
$scope.recommendations = []; $scope.states = [];
var workflows = Workflow.getAll(); $scope.recommendations = [];
_.forEach(workflows, function (workflow) { var workflows = $scope.collectAllUsedWorkflows();
var workflowHeader = { _.forEach(workflows, function (workflow) {
headername: workflow.name, if (workflows.length > 1) {
workflowHeader: true, var workflowHeader = {
}; headername: workflow.name,
$scope.states.push(workflowHeader); workflowHeader: true,
$scope.recommendations.push(workflowHeader); };
_.forEach(workflow.states, function (state) { $scope.states.push(workflowHeader);
$scope.states.push(state); $scope.recommendations.push(workflowHeader);
if (state.recommendation_label) {
$scope.recommendations.push(state);
} }
var firstEndStateSeen = false;
_.forEach(_.orderBy(workflow.states, 'id'), function (state) {
if (state.next_states_id.length === 0 && !firstEndStateSeen) {
$scope.states.push({divider: true});
firstEndStateSeen = true;
}
$scope.states.push(state);
if (state.recommendation_label) {
$scope.recommendations.push(state);
}
});
}); });
}); };
$scope.collectAllUsedWorkflows = function () {
return _.filter(Workflow.getAll(), function (workflow) {
return _.some($scope.motions, function (motion) {
return motion.state.workflow_id === workflow.id;
});
});
};
$scope.stateFilter = []; $scope.stateFilter = [];
var updateStateFilter = function () { var updateStateFilter = function () {
if (_.indexOf($scope.filter.multiselectFilters.state, -1) > -1) { // contains -1 $scope.stateFilter = _.clone($scope.filter.multiselectFilters.state);
$scope.stateFilter = _.filter($scope.filter.multiselectFilters.state, function (id) {
return id >= 0; var doneIndex = _.indexOf($scope.stateFilter, -1);
}); // remove -1 if (doneIndex > -1) { // contains -1 (done)
$scope.stateFilter.splice(doneIndex, 1); // remove -1
_.forEach($scope.states, function (state) { _.forEach($scope.states, function (state) {
if (!state.workflowHeader) { if (!state.workflowHeader && !state.divider) {
if (state.getNextStates().length === 0) { // done state if (state.next_states_id.length === 0) { // add all done state
$scope.stateFilter.push(state.id); $scope.stateFilter.push(state.id);
} }
} }
}); });
} else {
$scope.stateFilter = _.clone($scope.filter.multiselectFilters.state);
} }
var undoneIndex = _.indexOf($scope.stateFilter, -2);
if (undoneIndex > -1) { // contains -2 (undone)
$scope.stateFilter.splice(undoneIndex, 1); // remove -2
_.forEach($scope.states, function (state) {
if (!state.workflowHeader && !state.divider) {
if (state.next_states_id.length !== 0) { // add all undone state
$scope.stateFilter.push(state.id);
}
}
});
}
$scope.stateFilter = _.uniq($scope.stateFilter);
}; };
// Filtering // Filtering
@ -1025,8 +1055,10 @@ angular.module('OpenSlidesApp.motions.site', [
updateStateFilter(); updateStateFilter();
}; };
// Sorting // Sorting
$scope.sort = osTableSort.createInstance(); $scope.sort = osTableSort.createInstance('MotionTableSort');
$scope.sort.column = 'identifier'; if (!$scope.sort.column) {
$scope.sort.column = 'identifier';
}
$scope.sortOptions = [ $scope.sortOptions = [
{name: 'identifier', {name: 'identifier',
display_name: gettext('Identifier')}, display_name: gettext('Identifier')},
@ -1193,6 +1225,7 @@ angular.module('OpenSlidesApp.motions.site', [
'$http', '$http',
'$timeout', '$timeout',
'$window', '$window',
'$filter',
'operator', 'operator',
'ngDialog', 'ngDialog',
'gettextCatalog', 'gettextCatalog',
@ -1219,11 +1252,12 @@ angular.module('OpenSlidesApp.motions.site', [
'PersonalNoteManager', 'PersonalNoteManager',
'WebpageTitle', 'WebpageTitle',
'EditingWarning', 'EditingWarning',
function($scope, $http, $timeout, $window, operator, ngDialog, gettextCatalog, MotionForm, function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
ChangeRecommmendationCreate, ChangeRecommmendationView, MotionChangeRecommendation, MotionForm, ChangeRecommmendationCreate, ChangeRecommmendationView,
Motion, MotionComment, Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing, MotionChangeRecommendation, Motion, MotionComment, Category, Mediafile, Tag, User,
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock, MotionPdfExport, Workflow, Config, motionId, MotionInlineEditing, MotionCommentsInlineEditing, Editor,
PersonalNoteManager, WebpageTitle, EditingWarning) { Projector, ProjectionDefault, MotionBlock, MotionPdfExport, PersonalNoteManager,
WebpageTitle, EditingWarning) {
var motion = Motion.get(motionId); var motion = Motion.get(motionId);
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
@ -1263,6 +1297,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.recommendationExtension = $scope.motion.comments[$scope.commentFieldForRecommendationId]; $scope.recommendationExtension = $scope.motion.comments[$scope.commentFieldForRecommendationId];
} }
$scope.motion.personalNote = PersonalNoteManager.getNote($scope.motion); $scope.motion.personalNote = PersonalNoteManager.getNote($scope.motion);
$scope.navigation.evaluate();
var webpageTitle = gettextCatalog.getString('Motion') + ' '; var webpageTitle = gettextCatalog.getString('Motion') + ' ';
if ($scope.motion.identifier) { if ($scope.motion.identifier) {
@ -1355,6 +1390,17 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.save = function (motion) { $scope.save = function (motion) {
Motion.save(motion, {method: 'PATCH'}); Motion.save(motion, {method: 'PATCH'});
}; };
// Navigation buttons
$scope.navigation = {
evaluate: function () {
var motions = $filter('orderByEmptyLast')(Motion.getAll(), 'identifier');
var thisIndex = _.findIndex(motions, function (motion) {
return motion.id === $scope.motion.id;
});
this.nextMotion = thisIndex < motions.length-1 ? motions[thisIndex+1] : _.head(motions);
this.previousMotion = thisIndex > 0 ? motions[thisIndex-1] : _.last(motions);
},
};
// support // support
$scope.support = function () { $scope.support = function () {
$http.post('/rest/motions/motion/' + motion.id + '/support/'); $http.post('/rest/motions/motion/' + motion.id + '/support/');

View File

@ -65,18 +65,36 @@
ng-if="operator.user" ng-if="operator.user"
title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i> title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i>
</h1> </h1>
<h2> <div class="row">
<translate>Motion</translate> {{ motion.identifier }} <div class="col-sm-6">
<span ng-if="parent"> <h2>
(<translate>Amendment of motion</translate> <translate>Motion</translate> {{ motion.identifier }}
<a ui-sref="motions.motion.detail({id: parent.id})">{{ parent.identifier || parent.getTitle() }}</a>) <span ng-if="parent">
(<translate>Amendment of motion</translate>
<a ui-sref="motions.motion.detail({id: parent.id})">{{ parent.identifier || parent.getTitle() }}</a>)
</span>
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion(version).version_number }}</span>
<span ng-if="motion.active_version != version" class="label label-warning">
<i class="fa fa-exclamation-triangle"></i>
<translate>This version is not permitted.</translate>
</span>
</h2>
</div>
<div class="col-sm-6">
<span class="pull-right">
<a ui-sref="motions.motion.detail({id: navigation.previousMotion.id})" class="btn btn-default"
ng-disabled="!navigation.previousMotion">
<i class="fa fa-angle-double-left"></i>
<translate>Motion</translate> {{ navigation.previousMotion.identifier }}
</a>
<a ui-sref="motions.motion.detail({id: navigation.nextMotion.id})" class="btn btn-default"
ng-disabled="!navigation.nextMotion">
<translate>Motion</translate> {{ navigation.nextMotion.identifier }}
<i class="fa fa-angle-double-right"></i>
</a>
</span> </span>
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion(version).version_number }}</span> </div>
<span ng-if="motion.active_version != version" class="label label-warning"> </div>
<i class="fa fa-exclamation-triangle"></i>
<translate>This version is not permitted.</translate>
</span>
</h2>
</div> </div>
</div> </div>

View File

@ -157,11 +157,11 @@
<span class="caret"></span> <span class="caret"></span>
</span> </span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownState"> <ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownState">
<li ng-repeat="state in states" ng-class="state.workflowHeader ? 'dropdown-header' : ''"> <li ng-repeat="state in states" ng-class="{'dropdown-header': state.workflowHeader, 'divider': state.divider}">
<a ng-if="state.workflowHeader"> <a ng-if="state.workflowHeader">
{{ state.headername | translate }} {{ state.headername | translate }}
</a> </a>
<a href ng-if="!state.workflowHeader" <a href ng-if="!state.workflowHeader && !state.divider"
ng-click="operateStateFilter(state.id, isSelectMode)"> ng-click="operateStateFilter(state.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(state.id) > -1"></i> <i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(state.id) > -1"></i>
{{ state.name | translate }} {{ state.name | translate }}
@ -174,6 +174,12 @@
<translate>done</translate> <translate>done</translate>
</a> </a>
</li> </li>
<li>
<a href ng-click="operateStateFilter(-2, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(-2) > -1"></i>
<translate>undone</translate>
</a>
</li>
</ul> </ul>
</span> </span>
<!-- recommendation filter --> <!-- recommendation filter -->
@ -398,6 +404,12 @@
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
<translate>done</translate> <translate>done</translate>
</span> </span>
<span ng-if="filter.multiselectFilters.state.indexOf(-2) > -1" class="pointer spacer-left-lg"
ng-click="operateStateFilter(-2, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>undone</translate>
</span>
<!-- category --> <!-- category -->
<span ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')" <span ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')"
class="pointer spacer-left-lg" class="pointer spacer-left-lg"
@ -504,7 +516,7 @@
| filter: {star: filter.booleanFilters.isFavorite.value} | filter: {star: filter.booleanFilters.isFavorite.value}
| filter: {hasPersonalNote: filter.booleanFilters.hasPersonalNote.value} | filter: {hasPersonalNote: filter.booleanFilters.hasPersonalNote.value}
| toArray | toArray
| orderBy: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo : itemsPerPage : limitBegin"> | limitTo : itemsPerPage : limitBegin">
<!-- select column --> <!-- select column -->

View File

@ -2,13 +2,14 @@
<!-- Title --> <!-- Title -->
<div id="title"> <div id="title">
<h1>{{ motionBlock.agenda_item.getTitle() }}</h1> <h1>{{ motionBlock.agenda_item.getTitle() }}</h1>
<h2 translate>Motion block</h2> <h2><translate>Motion block</translate> &mdash; {{motionBlock.motions.length }} <translate>Motions</translate></h2>
</div> </div>
<!-- motion list --> <!-- motion list -->
<div style="display: flex; flex-wrap: wrap;"> <div class="motion-block">
<div ng-repeat="motion in motionBlock.motions" style="width: 33%;"> <div ng-repeat="motion in motionBlock.motions">
{{ motion.identifier }} {{ motion.identifier }}
<br>
<small> <small>
<span class="label" ng-class="'label-'+motion.recommendation.css_class"> <span class="label" ng-class="'label-'+motion.recommendation.css_class">
{{ motion.getRecommendationName() }} {{ motion.getRecommendationName() }}

View File

@ -595,8 +595,10 @@ angular.module('OpenSlidesApp.users.site', [
group: function (user) {return user.groups_id;}, group: function (user) {return user.groups_id;},
}; };
// Sorting // Sorting
$scope.sort = osTableSort.createInstance(); $scope.sort = osTableSort.createInstance('UserTableSort');
$scope.sort.column = $scope.config('users_sort_by'); if (!$scope.sort.column) {
$scope.sort.column = $scope.config('users_sort_by');
}
$scope.sortOptions = [ $scope.sortOptions = [
{name: 'first_name', {name: 'first_name',
display_name: gettext('Given name')}, display_name: gettext('Given name')},

View File

@ -147,7 +147,7 @@
| filter: {is_active: filter.booleanFilters.isActive.value} | filter: {is_active: filter.booleanFilters.isActive.value}
| filter: {is_committee: filter.booleanFilters.isCommittee.value} | filter: {is_committee: filter.booleanFilters.isCommittee.value}
| MultiselectFilter: filter.multiselectFilters.group : getItemId.group | MultiselectFilter: filter.multiselectFilters.group : getItemId.group
| orderBy: sort.column: sort.reverse)"></span> | orderByEmptyLast: sort.column: sort.reverse)"></span>
</div> </div>
<!-- filter users (for user without 'can_see_extra_data' permission) --> <!-- filter users (for user without 'can_see_extra_data' permission) -->
<div os-perms="!users.can_see_extra_data" <div os-perms="!users.can_see_extra_data"
@ -155,7 +155,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString | osFilter: filter.filterString : filter.getObjectQueryString
| filter: {is_committee: filter.booleanFilters.isCommittee.value} | filter: {is_committee: filter.booleanFilters.isCommittee.value}
| MultiselectFilter: filter.multiselectFilters.group : getItemId.group | MultiselectFilter: filter.multiselectFilters.group : getItemId.group
| orderBy: sort.column: sort.reverse)"></div> | orderByEmptyLast: sort.column: sort.reverse)"></div>
<div class="os-table container-fluid"> <div class="os-table container-fluid">
<div class="row header-row"> <div class="row header-row">