Motion optimizations

This commit is contained in:
FinnStutzenstein 2017-10-27 09:38:06 +02:00
parent a8ff32bbb1
commit 5e8662e470
14 changed files with 196 additions and 61 deletions

View File

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

View File

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

View File

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

View File

@ -48,6 +48,14 @@ body {
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 ***/
.pContainer {
background-color: #222;
@ -573,6 +581,18 @@ p.os-split-after {
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 ***/
img.projector-image {
width: 100%;

View File

@ -1420,8 +1420,8 @@ angular.module('OpenSlidesApp.core', [
])
// filters the requesting object (id=selfid) from a list of input objects
.filter('notself', function() {
return function(input, selfid) {
.filter('notself', function () {
return function (input, selfid) {
var result;
if (selfid) {
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
.run([
'ChatMessage',

View File

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

View File

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

View File

@ -240,7 +240,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString
| filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))}
| filter: {hidden: filter.booleanFilters.isHidden.value}
| orderBy: sort.column : sort.reverse )">
| orderByEmptyLast: sort.column : sort.reverse )">
<!-- select column -->
<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;
}
});
$scope.collectStatesAndRecommendations();
});
$scope.alert = {};
@ -917,40 +918,69 @@ angular.module('OpenSlidesApp.motions.site', [
};
// collect all states and all recommendations of all workflows
$scope.states = [];
$scope.recommendations = [];
var workflows = Workflow.getAll();
_.forEach(workflows, function (workflow) {
var workflowHeader = {
headername: workflow.name,
workflowHeader: true,
};
$scope.states.push(workflowHeader);
$scope.recommendations.push(workflowHeader);
_.forEach(workflow.states, function (state) {
$scope.states.push(state);
if (state.recommendation_label) {
$scope.recommendations.push(state);
$scope.collectStatesAndRecommendations = function () {
$scope.states = [];
$scope.recommendations = [];
var workflows = $scope.collectAllUsedWorkflows();
_.forEach(workflows, function (workflow) {
if (workflows.length > 1) {
var workflowHeader = {
headername: workflow.name,
workflowHeader: true,
};
$scope.states.push(workflowHeader);
$scope.recommendations.push(workflowHeader);
}
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 = [];
var updateStateFilter = function () {
if (_.indexOf($scope.filter.multiselectFilters.state, -1) > -1) { // contains -1
$scope.stateFilter = _.filter($scope.filter.multiselectFilters.state, function (id) {
return id >= 0;
}); // remove -1
$scope.stateFilter = _.clone($scope.filter.multiselectFilters.state);
var doneIndex = _.indexOf($scope.stateFilter, -1);
if (doneIndex > -1) { // contains -1 (done)
$scope.stateFilter.splice(doneIndex, 1); // remove -1
_.forEach($scope.states, function (state) {
if (!state.workflowHeader) {
if (state.getNextStates().length === 0) { // done state
if (!state.workflowHeader && !state.divider) {
if (state.next_states_id.length === 0) { // add all done state
$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
@ -1025,8 +1055,10 @@ angular.module('OpenSlidesApp.motions.site', [
updateStateFilter();
};
// Sorting
$scope.sort = osTableSort.createInstance();
$scope.sort.column = 'identifier';
$scope.sort = osTableSort.createInstance('MotionTableSort');
if (!$scope.sort.column) {
$scope.sort.column = 'identifier';
}
$scope.sortOptions = [
{name: 'identifier',
display_name: gettext('Identifier')},
@ -1193,6 +1225,7 @@ angular.module('OpenSlidesApp.motions.site', [
'$http',
'$timeout',
'$window',
'$filter',
'operator',
'ngDialog',
'gettextCatalog',
@ -1219,11 +1252,12 @@ angular.module('OpenSlidesApp.motions.site', [
'PersonalNoteManager',
'WebpageTitle',
'EditingWarning',
function($scope, $http, $timeout, $window, operator, ngDialog, gettextCatalog, MotionForm,
ChangeRecommmendationCreate, ChangeRecommmendationView, MotionChangeRecommendation,
Motion, MotionComment, Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock, MotionPdfExport,
PersonalNoteManager, WebpageTitle, EditingWarning) {
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
MotionForm, ChangeRecommmendationCreate, ChangeRecommmendationView,
MotionChangeRecommendation, Motion, MotionComment, Category, Mediafile, Tag, User,
Workflow, Config, motionId, MotionInlineEditing, MotionCommentsInlineEditing, Editor,
Projector, ProjectionDefault, MotionBlock, MotionPdfExport, PersonalNoteManager,
WebpageTitle, EditingWarning) {
var motion = Motion.get(motionId);
Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles');
@ -1263,6 +1297,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.recommendationExtension = $scope.motion.comments[$scope.commentFieldForRecommendationId];
}
$scope.motion.personalNote = PersonalNoteManager.getNote($scope.motion);
$scope.navigation.evaluate();
var webpageTitle = gettextCatalog.getString('Motion') + ' ';
if ($scope.motion.identifier) {
@ -1355,6 +1390,17 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.save = function (motion) {
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
$scope.support = function () {
$http.post('/rest/motions/motion/' + motion.id + '/support/');

View File

@ -65,18 +65,36 @@
ng-if="operator.user"
title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i>
</h1>
<h2>
<translate>Motion</translate> {{ motion.identifier }}
<span ng-if="parent">
(<translate>Amendment of motion</translate>
<a ui-sref="motions.motion.detail({id: parent.id})">{{ parent.identifier || parent.getTitle() }}</a>)
<div class="row">
<div class="col-sm-6">
<h2>
<translate>Motion</translate> {{ motion.identifier }}
<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 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>
</div>
</div>

View File

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

View File

@ -2,13 +2,14 @@
<!-- Title -->
<div id="title">
<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>
<!-- motion list -->
<div style="display: flex; flex-wrap: wrap;">
<div ng-repeat="motion in motionBlock.motions" style="width: 33%;">
<div class="motion-block">
<div ng-repeat="motion in motionBlock.motions">
{{ motion.identifier }}
<br>
<small>
<span class="label" ng-class="'label-'+motion.recommendation.css_class">
{{ motion.getRecommendationName() }}

View File

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

View File

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