Collapsable agenda, one panel for each motion comment and hide closed agenda items in the item slide.

This commit is contained in:
FinnStutzenstein 2018-02-06 12:12:52 +01:00
parent 8042beda60
commit 44fcfb447e
12 changed files with 132 additions and 75 deletions

View File

@ -15,6 +15,8 @@ Agenda:
- Fixed multiple request on creation of agenda related items [#3341]. - Fixed multiple request on creation of agenda related items [#3341].
- Added possibility to mark speakers [#3570]. - Added possibility to mark speakers [#3570].
- New DOCX export of agenda [#3569]. - New DOCX export of agenda [#3569].
- Hide closed agenda items in the item slide [#3567].
- Agenda is now collapsable for a better overview [#3567].
Motions: Motions:
- New export dialog [#3185]. - New export dialog [#3185].

View File

@ -6,10 +6,6 @@
p { p {
font-size: 140%; font-size: 140%;
&.done {
color: #9a9898;
}
} }
.mainitem { .mainitem {

View File

@ -2,12 +2,21 @@
#agenda-table { #agenda-table {
.icon-column { .icon-column {
padding: 3px;
width: 5%; width: 5%;
} }
.title-column { .title-column {
padding: 0px 10px; padding-left: 3px;
width: 95%; padding-right: 10px;
width: calc(95% - 15px );
}
.caret-spacer {
width: 15px;
padding: 3px;
color: #337ab7;
font-size: 115%;
} }
} }

View File

@ -298,6 +298,7 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
function generateFlatTree(tree, parentCount) { function generateFlatTree(tree, parentCount) {
_.each(tree, function (item) { _.each(tree, function (item) {
item.item.childrenCount = item.children.length;
item.item.parentCount = parentCount; item.item.parentCount = parentCount;
flatItems.push(item.item); flatItems.push(item.item);
generateFlatTree(item.children, parentCount + 1); generateFlatTree(item.children, parentCount + 1);

View File

@ -120,10 +120,7 @@ angular.module('OpenSlidesApp.agenda.site', [
return item.list_view_title; return item.list_view_title;
}); });
$scope.items = AgendaTree.getFlatTree(allowedItems); $scope.items = AgendaTree.getFlatTree(allowedItems);
var subitems = $filter('filter')($scope.items, {'parent_id': ''}); $scope.agendaHasSubitems = $filter('filter')($scope.items, {'parent_id': ''}).length;
if (subitems.length) {
$scope.agendaHasSubitems = true;
}
}); });
Projector.bindAll({}, $scope, 'projectors'); Projector.bindAll({}, $scope, 'projectors');
$scope.mainListTree = true; $scope.mainListTree = true;
@ -169,6 +166,12 @@ angular.module('OpenSlidesApp.agenda.site', [
}, },
}; };
// Expand all items during searching.
$scope.filter.changed = function () {
$scope.collapseState = true;
$scope.toggleCollapseState();
};
// pagination // pagination
$scope.pagination = osTablePagination.createInstance('AgendaTablePagination'); $scope.pagination = osTablePagination.createInstance('AgendaTablePagination');
@ -235,6 +238,14 @@ angular.module('OpenSlidesApp.agenda.site', [
} }
}; };
// Agenda collapse function
$scope.toggleCollapseState = function () {
$scope.collapseState = !$scope.collapseState;
_.forEach($scope.items, function (item) {
item.hideChildren = $scope.collapseState;
});
};
/** Agenda item functions **/ /** Agenda item functions **/
// open dialog for new topics // TODO Remove this. Don't forget import button in template. // open dialog for new topics // TODO Remove this. Don't forget import button in template.
$scope.newDialog = function () { $scope.newDialog = function () {
@ -278,6 +289,7 @@ angular.module('OpenSlidesApp.agenda.site', [
$scope.edit = function (item) { $scope.edit = function (item) {
ngDialog.open(item.getContentObjectForm().getDialog({id: item.content_object.id})); ngDialog.open(item.getContentObjectForm().getDialog({id: item.content_object.id}));
}; };
// export // export
$scope.pdfExport = function () { $scope.pdfExport = function () {
AgendaPdfExport.export($scope.itemsFiltered); AgendaPdfExport.export($scope.itemsFiltered);
@ -393,6 +405,33 @@ angular.module('OpenSlidesApp.agenda.site', [
} }
]) ])
// filter to hide collapsed items. Items has to be a flat tree.
.filter('collapsedItemFilter', [
function () {
return function (items) {
return _.filter(items, function (item) {
var index = _.findIndex(items, item);
var parentId = item.parent_id;
// Search for parents, if one has the hideChildren attribute set. All parents
// have a higher index as this item, because items is a flat tree.
// If a parent has this attribute, we should remove this item. Else if we hit
// the top or an item on the first layer, the item is not collapsed.
for (--index; index >= 0 && parentId !== null; index--) {
var p = items[index];
if (p.id === parentId) {
if (p.hideChildren) {
return false;
} else {
parentId = p.parent_id;
}
}
}
return true;
});
};
}
])
.controller('ItemDetailCtrl', [ .controller('ItemDetailCtrl', [
'$scope', '$scope',
'$filter', '$filter',

View File

@ -148,6 +148,13 @@
</span> </span>
</span> </span>
</span> </span>
<span ng-if="items.length === itemsFiltered.length">
&middot;
<a href="" ng-click="toggleCollapseState()">
<span ng-if="collapseState" translate>Expand all</span>
<span ng-if="!collapseState" translate>Collapse all</span>
</a>
</span>
</div> </div>
<div class="col-md-6" ng-show="itemsFiltered.length > pagination.itemsPerPage"> <div class="col-md-6" ng-show="itemsFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
@ -232,6 +239,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString | osFilter: filter.filterString : filter.getObjectQueryString
| filter: {closed: filter.booleanFilters.closed.value} | filter: {closed: filter.booleanFilters.closed.value}
| filter: {is_hidden: filter.booleanFilters.is_hidden.value}) | filter: {is_hidden: filter.booleanFilters.is_hidden.value})
| collapsedItemFilter
| limitTo : pagination.itemsPerPage : pagination.limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
@ -279,11 +287,18 @@
<div class="no-projector-spacer" os-perms="!core.can_manage_projector"></div> <div class="no-projector-spacer" os-perms="!core.can_manage_projector"></div>
<!-- main content column --> <!-- main content column -->
<div class="col-xs-6 content" style="padding-left: calc({{ item.parentCount }}*15px)"> <div class="col-xs-6 content"
style="padding-left: calc({{ items.length === itemsFiltered.length ? item.parentCount : 0 }}*25px)">
<div class="icon-column"> <div class="icon-column">
<i class="fa fa-ban" ng-style="{'visibility': item.is_hidden ? 'visible' : 'hidden'}" <i class="fa fa-ban" ng-style="{'visibility': item.is_hidden ? 'visible' : 'hidden'}"
title="{{ 'Internal item' | translate }}"></i> title="{{ 'Internal item' | translate }}"></i>
</div> </div>
<div class="caret-spacer" ng-if="items.length === itemsFiltered.length">
<i class="fa pointer"
ng-style="{visibility: item.childrenCount ? 'visible' : 'hidden'}"
ng-class="item.hideChildren ? 'fa-caret-right' : 'fa-caret-down'"
ng-click="item.hideChildren = !item.hideChildren"></i>
</div>
<div class="title-column"> <div class="title-column">
<!-- ID and title --> <!-- ID and title -->
<div> <div>
@ -295,7 +310,8 @@
</span> </span>
</div> </div>
<!-- hover menu --> <!-- hover menu -->
<div os-perms="agenda.can_see" ng-class="{'hiddenDiv': !item.hover}"> <div os-perms="agenda.can_see" ng-class="{'hiddenDiv': !item.hover}"
ng-style="{'padding-left': items.length === itemsFiltered.length ? '15px' : '0px'}">
<small> <small>
<a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a> <a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a>
<span os-perms="agenda.can_manage"> &middot; <span os-perms="agenda.can_manage"> &middot;

View File

@ -11,18 +11,18 @@
<!-- Nested node template --> <!-- Nested node template -->
<script type="text/ng-template" id="projector_agenda_renderer.html"> <script type="text/ng-template" id="projector_agenda_renderer.html">
<td class="number"> <td class="number" ng-if="!node.item.closed">
<p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null, done: node.item.closed}"> <p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null}">
{{ node.item.item_number }} {{ node.item.item_number }}
</p> </p>
</td> </td>
<td> <td ng-if="!node.item.closed">
<p ng-class="{mainitem: node.item.parent_id === null}" ng-if="node.item.item_number"> <p ng-class="{mainitem: node.item.parent_id === null}" ng-if="node.item.item_number">
&middot; &middot;
</p> </p>
</td> </td>
<td> <td ng-if="!node.item.closed">
<p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null, done: node.item.closed}"> <p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null}">
{{ node.item.title }} {{ node.item.title }}
</p> </p>
<table ng-if="node.children.length" class="agendalist-table"> <table ng-if="node.children.length" class="agendalist-table">

View File

@ -511,6 +511,7 @@ angular.module('OpenSlidesApp.core.site', [
self.existsStorageEntry = existsStorageEntry; self.existsStorageEntry = existsStorageEntry;
self.save = function () { self.save = function () {
$sessionStorage[tableName] = self; $sessionStorage[tableName] = self;
self.changed();
}; };
self.areFiltersSet = function () { self.areFiltersSet = function () {
var areFiltersSet = _.find(self.multiselectFilters, function (filterList) { var areFiltersSet = _.find(self.multiselectFilters, function (filterList) {
@ -567,6 +568,8 @@ angular.module('OpenSlidesApp.core.site', [
}); });
return stringList.join(' '); return stringList.join(' ');
}; };
// Stub for callback
self.changed = function () {};
return self; return self;
}; };

View File

@ -131,7 +131,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
function (MotionInlineEditing, Editor) { function (MotionInlineEditing, Editor) {
var createInstances = function ($scope, motion) { var createInstances = function ($scope, motion) {
var commentsInlineEditing = { var commentsInlineEditing = {
editors: [] editors: {}, // Map comment id to editor instance.
}; };
var options = Editor.getOptions('inline', 'YOffset'); var options = Editor.getOptions('inline', 'YOffset');
_.forEachRight($scope.noSpecialCommentsFields, function (field, id) { _.forEachRight($scope.noSpecialCommentsFields, function (field, id) {
@ -141,20 +141,20 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
return motion['comment_' + id]; return motion['comment_' + id];
}, },
function (obj) { function (obj) {
motion['comment_' + id] = obj.editor.getData(); if (obj.editor) {
motion['comment_' + id] = obj.editor.getData();
}
} }
); );
commentsInlineEditing.editors.push(inlineEditing); commentsInlineEditing.editors[id] = inlineEditing;
}); });
commentsInlineEditing.saveToolbarVisible = function () { commentsInlineEditing.saveToolbarVisible = function () {
return _.some(commentsInlineEditing.editors, function (instance) { return _.some(commentsInlineEditing.editors, function (instance) {
return instance.changed && instance.active; return instance.changed && instance.active;
}); });
}; };
commentsInlineEditing.active = function () { commentsInlineEditing.active = function (commentId) {
return _.some(commentsInlineEditing.editors, function (instance) { return commentsInlineEditing.editors[commentId].active;
return instance.active;
});
}; };
commentsInlineEditing.save = function () { commentsInlineEditing.save = function () {
_.forEach(commentsInlineEditing.editors, function (instance) { _.forEach(commentsInlineEditing.editors, function (instance) {
@ -166,15 +166,11 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
instance.revert(); instance.revert();
}); });
}; };
commentsInlineEditing.enable = function () { commentsInlineEditing.enable = function (commentId) {
_.forEach(commentsInlineEditing.editors, function (instance) { commentsInlineEditing.editors[commentId].enable();
instance.enable();
});
}; };
commentsInlineEditing.disable = function () { commentsInlineEditing.disable = function (commentId) {
_.forEach(commentsInlineEditing.editors, function (instance) { commentsInlineEditing.editors[commentId].disable();
instance.disable();
});
}; };
return commentsInlineEditing; return commentsInlineEditing;

View File

@ -1069,26 +1069,23 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
}); });
}); });
}, },
exportComments: function (motion, filename) { exportComment: function (motion, commentId, filename) {
var fields = MotionComment.getNoSpecialCommentsFields(); var field = MotionComment.getNoSpecialCommentsFields()[commentId];
var content = []; if (field && motion.comments[commentId]) {
_.forEach(fields, function (field, id) { var title = field.name;
if (motion.comments[id]) { if (!field.public) {
var title = field.name; title += ' (' + gettextCatalog.getString('internal') + ')';
if (!field.public) {
title += ' (' + gettextCatalog.getString('internal') + ')';
}
content.push({
heading: title,
text: motion.comments[id],
});
} }
}); var content = [{
MotionPartialContentProvider.createInstance(motion, content).then(function (contentProvider) { heading: title,
PdfMakeDocumentProvider.createInstance(contentProvider).then(function (documentProvider) { text: motion.comments[commentId],
PdfCreate.download(documentProvider.getDocument(), filename); }];
MotionPartialContentProvider.createInstance(motion, content).then(function (contentProvider) {
PdfMakeDocumentProvider.createInstance(contentProvider).then(function (documentProvider) {
PdfCreate.download(documentProvider.getDocument(), filename);
});
}); });
}); }
}, },
}; };
} }

View File

@ -1765,11 +1765,11 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.createPollPdf = function () { $scope.createPollPdf = function () {
MotionPdfExport.createPollPdf($scope.motion, $scope.version); MotionPdfExport.createPollPdf($scope.motion, $scope.version);
}; };
$scope.exportComments = function () { $scope.exportComment = function (commentId) {
var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : ''; var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : '';
var commentsString = ' - ' + gettextCatalog.getString('Comments'); var commentsString = ' - ' + gettextCatalog.getString('Comments');
var filename = gettextCatalog.getString('Motion') + identifier + commentsString + '.pdf'; var filename = gettextCatalog.getString('Motion') + identifier + commentsString + '.pdf';
MotionPdfExport.exportComments($scope.motion, filename); MotionPdfExport.exportComment($scope.motion, commentId, filename);
}; };
$scope.exportPersonalNote = function () { $scope.exportPersonalNote = function () {
var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : ''; var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : '';

View File

@ -1,48 +1,46 @@
<div class="details" ng-if="commentFieldsAvailable()"> <div class="details" ng-repeat="(id, field) in noSpecialCommentsFields">
<div class="row"> <div class="row">
<!-- inline editing toolbar --> <!-- inline editing toolbar -->
<div class="motion-toolbar"> <div class="motion-toolbar">
<div class="pull-right inline-editing-activator"> <div class="pull-right inline-editing-activator">
<button ng-click="exportComments()" class="btn btn-default btn-sm" <button ng-click="exportComment(id)" class="btn btn-default btn-sm"
uib-tooltip="{{ 'Export comments only' | translate }}"> uib-tooltip="{{ 'Export comments only' | translate }}">
<i class="fa fa-file-pdf-o"></i> <i class="fa fa-file-pdf-o"></i>
<translate>PDF</translate> <translate>PDF</translate>
</button> </button>
<span ng-if="motion.isAllowed('change_comments')"> <span ng-if="motion.isAllowed('change_comments')">
<button ng-if="!commentsInlineEditing.active()" ng-click="commentsInlineEditing.enable()" <button ng-if="!commentsInlineEditing.active(id)" ng-click="commentsInlineEditing.enable(id)"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<i class="fa fa-pencil-square-o"></i> <i class="fa fa-pencil-square-o"></i>
<translate>Inline editing</translate> <translate>Inline editing</translate>
</button> </button>
<button ng-if="commentsInlineEditing.active()" ng-click="commentsInlineEditing.disable()" <button ng-if="commentsInlineEditing.active(id)" ng-click="commentsInlineEditing.disable(id)"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
<translate>Inline editing</translate> <translate>Inline editing</translate>
</button> </button>
</span> </span>
</div> </div>
<h1 class="toolbar-left" translate>Comments</h1> <h1 class="toolbar-left" translate>Comments
{{ field.name }}
<span ng-if="!field.public" class="label label-warning" translate>internal</span>
</h1>
</div> </div>
<!-- comment fields --> <!-- comment fields -->
<div class="col-sm-12"> <div class="col-sm-12">
<div ng-repeat="(id, field) in noSpecialCommentsFields"> <div id="view-original-comment-inline-editor-{{ id }}" style="min-height: 14px;"
<h4> ng-bind-html="motion.comments[id] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
{{ field.name }}
<span ng-if="!field.public" class="label label-warning" translate>internal</span>
</h4>
<div id="view-original-comment-inline-editor-{{ id }}" style="min-height: 14px;"
ng-bind-html="motion.comments[id] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
</div>
<!-- save toolbar -->
<div class="motion-save-toolbar" ng-class="{ 'visible': commentsInlineEditing.saveToolbarVisible() }">
<div class="changed-hint" translate>A comment has been changed.</div>
<button type="button" ng-click="commentsInlineEditing.save()" class="btn btn-primary" translate>
Save
</button>
<button type="button" ng-click="commentsInlineEditing.revert()" class="btn btn-default" translate>
Revert
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- save toolbar -->
<div class="motion-save-toolbar" ng-class="{ 'visible': commentsInlineEditing.saveToolbarVisible() }">
<div class="changed-hint" translate>A comment has been changed.</div>
<button type="button" ng-click="commentsInlineEditing.save()" class="btn btn-primary" translate>
Save
</button>
<button type="button" ng-click="commentsInlineEditing.revert()" class="btn btn-default" translate>
Revert
</button>
</div>