Merge pull request #3459 from FinnStutzenstein/MotionOptimizations
Motion optimizations
This commit is contained in:
commit
4a2d09e56c
@ -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].
|
||||
|
@ -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')},
|
||||
|
@ -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 -->
|
||||
|
@ -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%;
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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')},
|
||||
|
@ -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">
|
||||
|
@ -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/');
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 -->
|
||||
|
@ -2,13 +2,14 @@
|
||||
<!-- Title -->
|
||||
<div id="title">
|
||||
<h1>{{ motionBlock.agenda_item.getTitle() }}</h1>
|
||||
<h2 translate>Motion block</h2>
|
||||
<h2><translate>Motion block</translate> — {{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() }}
|
||||
|
@ -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')},
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user