diff --git a/CHANGELOG b/CHANGELOG index 55d13353b..7d3fcbf2d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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]. diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js index e0295952d..8a0da0c43 100644 --- a/openslides/assignments/static/js/assignments/site.js +++ b/openslides/assignments/static/js/assignments/site.js @@ -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')}, diff --git a/openslides/assignments/static/templates/assignments/assignment-list.html b/openslides/assignments/static/templates/assignments/assignment-list.html index 50512ea81..e396c2a23 100644 --- a/openslides/assignments/static/templates/assignments/assignment-list.html +++ b/openslides/assignments/static/templates/assignments/assignment-list.html @@ -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"> diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css index cb1e5458a..9cc6b2db6 100644 --- a/openslides/core/static/css/projector.css +++ b/openslides/core/static/css/projector.css @@ -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%; diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index baa3a32b5..9455bdbb1 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -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', diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index b033c7f13..dd00d3e73 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -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; }; diff --git a/openslides/mediafiles/static/js/mediafiles/list.js b/openslides/mediafiles/static/js/mediafiles/list.js index c9c7f1282..363f67c34 100644 --- a/openslides/mediafiles/static/js/mediafiles/list.js +++ b/openslides/mediafiles/static/js/mediafiles/list.js @@ -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')}, diff --git a/openslides/mediafiles/static/templates/mediafiles/mediafile-list.html b/openslides/mediafiles/static/templates/mediafiles/mediafile-list.html index 5669d8a6d..a9262b510 100644 --- a/openslides/mediafiles/static/templates/mediafiles/mediafile-list.html +++ b/openslides/mediafiles/static/templates/mediafiles/mediafile-list.html @@ -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 )">
diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index e2adecc5e..155df53df 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -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/'); diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index d7ca5c3ff..2c4e3c052 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -65,18 +65,36 @@ ng-if="operator.user" title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"> -

- Motion {{ motion.identifier }} - - (Amendment of motion - {{ parent.identifier || parent.getTitle() }}) +
+
+

+ Motion {{ motion.identifier }} + + (Amendment of motion + {{ parent.identifier || parent.getTitle() }}) + + | Version {{ motion.getVersion(version).version_number }} + + + This version is not permitted. + +

+
+
+ + + + Motion {{ navigation.previousMotion.identifier }} + + + Motion {{ navigation.nextMotion.identifier }} + + - | Version {{ motion.getVersion(version).version_number }} - - - This version is not permitted. - -

+
+ diff --git a/openslides/motions/static/templates/motions/motion-list.html b/openslides/motions/static/templates/motions/motion-list.html index 0443817fc..508c87378 100644 --- a/openslides/motions/static/templates/motions/motion-list.html +++ b/openslides/motions/static/templates/motions/motion-list.html @@ -157,11 +157,11 @@ @@ -398,6 +404,12 @@ done + + + undone + diff --git a/openslides/motions/static/templates/motions/slide_motion_block.html b/openslides/motions/static/templates/motions/slide_motion_block.html index 4a998945b..9729a3a31 100644 --- a/openslides/motions/static/templates/motions/slide_motion_block.html +++ b/openslides/motions/static/templates/motions/slide_motion_block.html @@ -2,13 +2,14 @@

{{ motionBlock.agenda_item.getTitle() }}

-

Motion block

+

Motion block — {{motionBlock.motions.length }} Motions

-
-
+
+
{{ motion.identifier }} +
{{ motion.getRecommendationName() }} diff --git a/openslides/users/static/js/users/site.js b/openslides/users/static/js/users/site.js index cba6e29b4..fd11d271c 100644 --- a/openslides/users/static/js/users/site.js +++ b/openslides/users/static/js/users/site.js @@ -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')}, diff --git a/openslides/users/static/templates/users/user-list.html b/openslides/users/static/templates/users/user-list.html index 922affa7c..2ee3d2f9c 100644 --- a/openslides/users/static/templates/users/user-list.html +++ b/openslides/users/static/templates/users/user-list.html @@ -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)"> + | orderByEmptyLast: sort.column: sort.reverse)">
+ | orderByEmptyLast: sort.column: sort.reverse)">