Merge pull request #2277 from FinnStutzenstein/FeatureCSV

Improved motions table with CSV export
This commit is contained in:
Emanuel Schütze 2016-09-08 14:47:54 +02:00 committed by GitHub
commit 2753af3585
5 changed files with 674 additions and 251 deletions

View File

@ -96,6 +96,21 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
} }
return title; return title;
}, },
getItemNumberWithAncestors: function (agendaId) {
if (!agendaId) {
agendaId = this.id;
}
var agendaItem = DS.get(name, agendaId);
if (!agendaItem) {
return '';
} else if (agendaItem.item_number) {
return agendaItem.item_number;
} else if (agendaItem.parent_id) {
return this.getItemNumberWithAncestors(agendaItem.parent_id);
} else {
return '';
}
},
// override project function of jsDataModel factory // override project function of jsDataModel factory
project: function() { project: function() {
return $http.post( return $http.post(

View File

@ -670,6 +670,100 @@ img {
z-index: 1; z-index: 1;
} }
/** Motion **/
#motion-table .row {
border: 1px solid #ddd;
border-top: 0px;
}
#motion-table .data-row:hover {
background-color: #f5f5f5;
}
#motion-table .data-row > div {
padding: 5px;
}
#motion-table .header-row {
border-top: 1px solid #ddd;
background-color: #f5f5f5;
}
.header-row > div {
padding: 10px;
}
#motion-table .main-header {
width: calc(100% - 50px);
float: right;
}
#motion-table .main-header .form-inline {
margin-left: 15px;
}
#motion-table .content > div {
display: inline-block;
float: left;
}
#motion-table .identifier-col {
width: 50px;
min-height: 1px;
}
#motion-table .identifier-col > div {
text-align: center;
}
#motion-table .title-col {
width: calc(100% - 50px);
}
#motion-table .title-col, #motion-table small {
color: #555;
}
#motion-table .content > div > div {
margin-bottom: 5px;
}
#motion-table .content > div > div:last-child {
margin-bottom: 0px;
}
#motion-table .row .centered {
text-align: center;
}
#motion-table .row .col-xs-1 {
width: 50px;
}
#motion-table .row .col-xs-4 {
padding-right: 10px;
}
#motion-table .dropdown {
display: inline-block;
}
#motion-table .dropdown > span, #motion-table .sort-spacer {
padding: 5px 10px 5px 10px;
}
#motion-table .dropdown-entry {
padding: 5px 10px 5px 10px;
display: inline-block;
width: 100%;
}
#motion-table .title {
margin-right: 10px;
padding: 0;
background-color: transparent;
}
/** Footer **/ /** Footer **/
#footer { #footer {
float: left; float: left;
@ -681,11 +775,23 @@ img {
/** General helper classes **/ /** General helper classes **/
.disabled {
color: #555;
cursor: not-allowed !important;
}
.bold {
font-weight: bold;
}
.btn-primary { .btn-primary {
background-color: #317796; background-color: #317796;
} }
.dropdown-menu {
margin-left: 0px !important;
}
.dropdown-entries { .dropdown-entries {
white-space: nowrap; white-space: nowrap;
} }
@ -717,6 +823,14 @@ img {
margin-right: 5px; margin-right: 5px;
} }
.spacer-left {
margin-left: 5px;
}
.spacer-left-lg {
margin-left: 10px;
}
.lead-div { .lead-div {
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -764,6 +878,10 @@ img {
width: 50%; width: 50%;
} }
.badge-info {
background-color: #f0ad4e;
}
.listOfSpeakers h3 { .listOfSpeakers h3 {
padding-bottom: 0; padding-bottom: 0;
} }
@ -1053,7 +1171,7 @@ tr.hiddenrow td {
} }
tr.activeline td, li.activeline, .projected { tr.activeline td, li.activeline, .projected {
background-color: #bed4de; background-color: #bed4de !important;
} }
tr.selected td { tr.selected td {
@ -1119,11 +1237,11 @@ tr.selected td {
#chatbox { width: 100%; top: 40px; } #chatbox { width: 100%; top: 40px; }
/* hide marked element / column */
.optional, .hide-sm { display: none; }
/* show replacement elements, if any */ /* show replacement elements, if any */
.optional-show { display: block !important; } .optional-show { display: block !important; }
/* hide marked element / column */
.optional, .hide-sm { display: none !important; }
} }
/* display for resolutions smaller that 560px */ /* display for resolutions smaller that 560px */

View File

@ -460,8 +460,7 @@ angular.module('OpenSlidesApp.core', [
]) ])
.filter('osFilter', [ .filter('osFilter', [
'$filter', function () {
function ($filter) {
return function (array, string, getFilterString) { return function (array, string, getFilterString) {
if (!string) { if (!string) {
return array; return array;
@ -473,6 +472,30 @@ angular.module('OpenSlidesApp.core', [
} }
]) ])
// This filter filters all items in array. If the filterArray is empty, the array is passed.
// The filterArray contains numbers of the multiselect: [1, 3, 4].
// Then, all items in array are passed, if the item_id (get with id_function) matches one of the
// ids in filterArray. id_function could also return a list of ids. Example:
// Item 1 has two tags with ids [1, 4]. filterArray = [3, 4] --> match
.filter('SelectMultipleFilter', [
function () {
return function (array, filterArray, idFunction) {
if (filterArray.length === 0) {
return array;
}
return Array.prototype.filter.call(array, function (item) {
var id = idFunction(item);
if (!id) {
return false;
} else if (typeof id === 'number') {
id = [id];
}
return _.intersection(id, filterArray).length > 0;
});
};
}
])
// mark HTML as "trusted" // mark HTML as "trusted"
.filter('trusted', [ .filter('trusted', [
'$sce', '$sce',

View File

@ -793,6 +793,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
.controller('MotionListCtrl', [ .controller('MotionListCtrl', [
'$scope', '$scope',
'$state', '$state',
'$http',
'ngDialog', 'ngDialog',
'MotionForm', 'MotionForm',
'Motion', 'Motion',
@ -800,7 +801,8 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'Tag', 'Tag',
'Workflow', 'Workflow',
'User', 'User',
function($scope, $state, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User) { 'Agenda',
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda) {
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
@ -812,6 +814,32 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
$scope.sortColumn = 'identifier'; $scope.sortColumn = 'identifier';
$scope.filterPresent = ''; $scope.filterPresent = '';
$scope.reverse = false; $scope.reverse = false;
$scope.multiselectFilter = {
state: [],
category: [],
tag: []
};
$scope.getItemId = {
state: function (motion) {return motion.state_id;},
category: function (motion) {return motion.category_id;},
tag: function (motion) {return motion.tags_id;}
};
// function to operate the multiselectFilter
$scope.operateMultiselectFilter = function (filter, id) {
if (!$scope.isDeleteMode) {
if (_.indexOf($scope.multiselectFilter[filter], id) > -1) {
// remove id
$scope.multiselectFilter[filter] = _.filter($scope.multiselectFilter[filter], function (_id) {
return _id != id;
});
} else {
// add id
$scope.multiselectFilter[filter].push(id);
}
}
};
// function to sort by clicked column // function to sort by clicked column
$scope.toggleSort = function (column) { $scope.toggleSort = function (column) {
if ( $scope.sortColumn === column ) { if ( $scope.sortColumn === column ) {
@ -852,6 +880,23 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
category, category,
].join(" "); ].join(" ");
}; };
// for reset-button
$scope.reset_filters = function () {
$scope.multiselectFilter = {
state: [],
category: [],
tag: []
};
if ($scope.filter) {
$scope.filter.search = '';
}
};
$scope.are_filters_set = function () {
return $scope.multiselectFilter.state.length > 0 ||
$scope.multiselectFilter.category.length > 0 ||
$scope.multiselectFilter.tag.length > 0 ||
($scope.filter ? $scope.filter.search : false);
};
// collect all states of all workflows // collect all states of all workflows
// TODO: regard workflows only which are used by motions // TODO: regard workflows only which are used by motions
@ -861,7 +906,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
if (workflows.length > 1) { if (workflows.length > 1) {
var wf = {}; var wf = {};
wf.name = workflow.name; wf.name = workflow.name;
wf.workflowSeparator = "-"; wf.workflowHeader = true;
$scope.states.push(wf); $scope.states.push(wf);
} }
angular.forEach(workflow.states, function (state) { angular.forEach(workflow.states, function (state) {
@ -869,41 +914,83 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
}); });
}); });
// update state
$scope.updateState = function (motion, state_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
};
// reset state
$scope.reset_state = function (motion) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
};
$scope.has_tag = function (motion, tag) {
return _.indexOf(motion.tags_id, tag.id) > -1;
};
// Use this methon instead of Motion.save(), because otherwise
// you have to provide always a title and a text
var save = function (motion) {
motion.title = motion.getTitle(-1);
motion.text = motion.getText(-1);
motion.reason = motion.getReason(-1);
Motion.save(motion);
};
$scope.toggle_tag = function (motion, tag) {
if ($scope.has_tag(motion, tag)) {
// remove
motion.tags_id = _.filter(motion.tags_id, function (tag_id){
return tag_id != tag.id;
});
} else {
motion.tags_id.push(tag.id);
}
save(motion);
};
$scope.toggle_category = function (motion, category) {
if (motion.category_id == category.id) {
motion.category_id = null;
} else {
motion.category_id = category.id;
}
save(motion);
};
// open new/edit dialog // open new/edit dialog
$scope.openDialog = function (motion) { $scope.openDialog = function (motion) {
ngDialog.open(MotionForm.getDialog(motion)); ngDialog.open(MotionForm.getDialog(motion));
}; };
// cancel QuickEdit mode
$scope.cancelQuickEdit = function (motion) { // Export the given motions as a csv file
// revert all changes by restore (refresh) original motion object from server $scope.csv_export = function () {
Motion.refresh(motion); var element = document.getElementById('downloadLink');
motion.quickEdit = false; var csvRows = [
}; ['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'],
// save changed motion ];
$scope.save = function (motion) { angular.forEach($scope.motionsFiltered, function (motion) {
// get (unchanged) values from latest version for update method var row = [];
motion.title = motion.getTitle(-1); row.push('"' + motion.identifier + '"');
motion.text = motion.getText(-1); row.push('"' + motion.getTitle() + '"');
motion.reason = motion.getReason(-1); row.push('"' + motion.getText() + '"');
Motion.save(motion).then( row.push('"' + motion.getReason() + '"');
function(success) { row.push('"' + motion.submitters[0].get_full_name() + '"');
motion.quickEdit = false; var category = motion.category ? motion.category.name : '';
$scope.alert.show = false; row.push('"' + category + '"');
}, row.push('"' + motion.origin + '"');
function(error){ csvRows.push(row);
var message = ''; });
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' '; var csvString = csvRows.join("%0A");
} element.href = 'data:text/csv;charset=utf-8,' + csvString;
$scope.alert = { type: 'danger', msg: message, show: true }; element.download = 'motions-export.csv';
}); element.target = '_blank';
}; };
// *** delete mode functions *** // *** delete mode functions ***
$scope.isDeleteMode = false; $scope.isDeleteMode = false;
// check all checkboxes // check all checkboxes from filtered motions
$scope.checkAll = function () { $scope.checkAll = function () {
angular.forEach($scope.motions, function (motion) { $scope.selectedAll = !$scope.selectedAll;
angular.forEach($scope.motionsFiltered, function (motion) {
motion.selected = $scope.selectedAll; motion.selected = $scope.selectedAll;
}); });
}; };
@ -918,7 +1005,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
}; };
// delete selected motions // delete selected motions
$scope.deleteMultiple = function () { $scope.deleteMultiple = function () {
angular.forEach($scope.motions, function (motion) { angular.forEach($scope.motionsFiltered, function (motion) {
if (motion.selected) if (motion.selected)
Motion.destroy(motion.id); Motion.destroy(motion.id);
}); });

View File

@ -17,10 +17,6 @@
<i class="fa fa-download fa-lg"></i> <i class="fa fa-download fa-lg"></i>
<translate>Import</translate> <translate>Import</translate>
</a> </a>
<a ui-sref="motions_pdf" target="_blank" class="btn btn-default btn-sm">
<i class="fa fa-file-pdf-o fa-lg"></i>
<translate>PDF</translate>
</a>
</div> </div>
<h1 translate>Motions</h1> <h1 translate>Motions</h1>
</div> </div>
@ -28,7 +24,7 @@
<div class="details"> <div class="details">
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-12">
<!-- delete mode --> <!-- delete mode -->
<button os-perms="motions.can_manage" class="btn" <button os-perms="motions.can_manage" class="btn"
ng-class="$parent.isDeleteMode ? 'btn-primary' : 'btn-default'" ng-class="$parent.isDeleteMode ? 'btn-primary' : 'btn-default'"
@ -36,38 +32,39 @@
<i class="fa fa-check-square-o"></i> <i class="fa fa-check-square-o"></i>
<translate>Select ...</translate> <translate>Select ...</translate>
</button> </button>
</div> <!-- Export dropdown -->
<div class="col-sm-6"> <div class="dropdown pull-right" uib-dropdown>
<div class="form-inline text-right"> <button type=button" class="btn btn-default" id="dropdownExport" uib-dropdown-toggle>
<div class="form-group"> <i class="fa fa-upload"></i>
<div class="input-group"> <span ng-if="motionsFiltered.length == motions.length" translate>
<div class="input-group-addon"><i class="fa fa-search"></i></div> Export all
<input type="text" ng-model="filter.search" class="form-control" </span>
placeholder="{{ 'Search' | translate}}"> <span ng-if="motionsFiltered.length != motions.length" translate>
</div> Export filtered
</div> </span>
<button class="btn btn-default" ng-click="isFilterOpen = !isFilterOpen" <span class="caret"></span>
ng-class="isFilterOpen ? 'btn-primary' : 'btn-default'">
<i class="fa fa-filter"></i>
<translate>Filter ...</translate>
</button> </button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
<li>
<a ui-sref="motions_pdf" target="_blank">
<i class="fa fa-file-pdf-o fa-lg"></i>
<translate>PDF</translate>
</a>
</li>
<!--CSV export -->
<li>
<a href="" id="downloadLink"
os-perms="motions.can_manage"
ng-click="csv_export()">
<i class="fa fa-file-text-o fa-lg"></i>
<translate>CSV</translate>
</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
<div uib-collapse="!isFilterOpen" class="row spacer">
<div class="col-sm-6 text-right"></div>
<div class="col-sm-6 text-right">
<!-- state filter -->
<select ng-model="stateFilter" class="form-control" id="stateFilter">
<option value="" translate>--- Select state ---</option>
<option ng-repeat="state in states" value="{{ state.id }}">
{{ state.workflowSeparator }}
{{ state.name | translate }}
{{ state.workflowSeparator }}
</option>
</select>
</div>
</div>
<div uib-collapse="!isDeleteMode" class="row spacer"> <div uib-collapse="!isDeleteMode" class="row spacer">
<div class="col-sm-12 text-left"> <div class="col-sm-12 text-left">
<!-- delete button --> <!-- delete button -->
@ -85,194 +82,377 @@
{{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">, {{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">,
{{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<!-- projector column -->
<th ng-show="!$parent.isDeleteMode" os-perms="core.can_manage_projector" class="minimum">
<!-- delete selection column -->
<th ng-show="$parent.isDeleteMode" os-perms="motions.can_manage" class="minimum deleteColumn">
<input type="checkbox" ng-model="$parent.selectedAll" ng-change="checkAll()">
<!-- agenda item column --> <div id="motion-table" class="container-fluid">
<th ng-click="toggleSort('agenda_item.item_number')" class="sortable optional"> <div class="row header-row">
<translate translate-comment="short form of agenda item">Item</translate> <div class="col-xs-1 centered" ng-show="isDeleteMode">
<i class="pull-right fa" ng-show="sortColumn === 'agenda_item.item_number' && header.sortable != false" <i class="fa text-danger pointer" ng-class=" selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-click="checkAll()"></i>
</i> </div>
<!-- identifier column --> <div class="col-xs-11 main-header">
<th ng-click="toggleSort('identifier')" class="sortable minimum">
<translate>Identifier</translate>
<i class="pull-right fa" ng-show="sortColumn === 'identifier' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
<!-- title column --> <span class="form-inline text-right pull-right">
<th ng-click="toggleSort('getTitle()')" class="sortable"> <span class="sort-spacer pointer" ng-click="reset_filters()"
<translate>Title</translate> ng-if="are_filters_set()" ng-disabled="isDeleteMode"
<i class="pull-right fa" ng-show="sortColumn === 'getTitle()' && header.sortable != false" ng-class="{'disabled': isDeleteMode}">
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> <i class="fa fa-times-circle"></i>
</i> <translate>Filter</translate>
</span>
<!-- submitters column --> <!-- Statefilter -->
<th ng-click="toggleSort('submitters')" class="sortable optional"> <span class="dropdown" uib-dropdown>
<translate>Submitters</translate> <span class="pointer" id="dropdownState" uib-dropdown-toggle
<i class="pull-right fa" ng-show="sortColumn === 'submitters' && header.sortable != false" ng-class="{'bold': multiselectFilter.state.length > 0, 'disabled': isDeleteMode}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-disabled="isDeleteMode">
</i> <translate>State</translate>
<span class="caret"></span>
<!-- category column --> </span>
<th ng-click="toggleSort('category')" class="sortable optional"> <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownState">
<translate>Category</translate> <li ng-repeat="state in states" ng-class="state.workflowHeader ? 'dropdown-header' : ''">
<i class="pull-right fa" ng-show="sortColumn === 'category' && header.sortable != false" <div class="dropdown-entry pointer" ng-if="state.workflowHeader">
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> {{ state.name | translate }}
</i> </div>
<div class="dropdown-entry pointer" ng-if="!state.workflowHeader"
<!-- state column --> ng-click="operateMultiselectFilter('state', state.id)">
<th ng-click="toggleSort('state.name')" class="sortable optional"> <i class="fa fa-check" ng-if="multiselectFilter.state.indexOf(state.id) > -1"></i>
<translate>State</translate> {{ state.name | translate }}
<i class="pull-right fa" ng-show="sortColumn === 'state.name' && header.sortable != false" </div>
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> </li>
</i> </ul>
<tbody> </span>
<tr ng-repeat="motion in motionsFiltered = (motions | osFilter: filter.search : getFilterString | <!-- Categoryfilter -->
filter: {state_id: stateFilter} | orderBy: sortColumn:reverse)" <span class="dropdown" uib-dropdown ng-if="categories.length > 0">
class="animate-item" <span class="pointer" id="dropdownCategory" uib-dropdown-toggle
ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }"> ng-class="{'bold': multiselectFilter.category.length > 0, 'disabled': isDeleteMode}"
ng-disabled="isDeleteMode">
<!-- projector --> <translate>Category</translate>
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector"> <span class="caret"></span>
<a class="btn btn-default btn-sm" </span>
ng-class="{ 'btn-primary': motion.isProjected() }" <ul class="dropdown-menu dropdown-menu-right"
ng-click="motion.project()" aria-labelledby="dropdownCategory">
title="{{ 'Project motion' | translate }}"> <li ng-repeat="category in categories">
<i class="fa fa-video-camera"></i> <div class="dropdown-entry pointer"
</a> ng-click="operateMultiselectFilter('category', category.id)">
<i class="fa fa-check" ng-if="multiselectFilter.category.indexOf(category.id) > -1"></i>
<!-- delete selection --> {{ category.name }}
<td ng-show="isDeleteMode" os-perms="motions.can_manage" class="deleteColumn"> </div>
<input type="checkbox" ng-model="motion.selected"> </li>
</ul>
<!-- agenda item number --> </span>
<td ng-if="!motion.quickEdit" class="optional">{{ motion.agenda_item.item_number }} <!-- Tagfilter -->
<span class="dropdown" uib-dropdown ng-if="tags.length > 0">
<!-- identifier --> <span class="pointer" id="dropdownTag" uib-dropdown-toggle
<td ng-if="!motion.quickEdit">{{ motion.identifier }} ng-class="{'bold': multiselectFilter.tag.length > 0, 'disabled': isDeleteMode}"
ng-disabled="isDeleteMode">
<!-- title --> <translate>Tag</translate>
<td ng-if="!motion.quickEdit" ng-mouseover="motion.hover=true" ng-mouseleave="motion.hover=false"> <span class="caret"></span>
<strong><a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.getTitle() }}</a></strong> </span>
<span ng-repeat="tag in motion.tags" class="label label-default"> <ul class="dropdown-menu dropdown-menu-right"
aria-labelledby="dropdownTag">
<li ng-repeat="tag in tags">
<div class="dropdown-entry pointer"
ng-click="operateMultiselectFilter('tag', tag.id)">
<i class="fa fa-check" ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"></i>
{{ tag.name }}
</div>
</li>
</ul>
</span>
<!-- dropdown sort -->
<span class="dropdown" uib-dropdown>
<span class="pointer" id="dropdownSort" uib-dropdown-toggle
ng-class="{'disabled': isDeleteMode}"
ng-disabled="isDeleteMode">
<translate>Sort</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownSort">
<li>
<!-- item -->
<div class="pointer dropdown-entry" ng-click="toggleSort('agenda_item.getItemNumberWithAncestors()')">
<translate translate-comment="short form of agenda item">Item</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'agenda_item.getItemNumberWithAncestors()' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
<li>
<!-- indentifier -->
<div class="pointer dropdown-entry" ng-click="toggleSort('identifier')">
<translate>Identifier</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'identifier' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
<li>
<!-- title -->
<div class="pointer dropdown-entry" ng-click="toggleSort('getTitle()')">
<translate>Title</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'getTitle()' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
<li>
<!-- submitters -->
<div class="pointer dropdown-entry" ng-click="toggleSort('submitters')">
<translate>Submitters</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'submitters' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
<li>
<!-- category -->
<div class="pointer dropdown-entry" ng-click="toggleSort('category')">
<translate>Category</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'category' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
<li>
<!-- state -->
<div class="pointer dropdown-entry" ng-click="toggleSort('state.name')">
<translate>State</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sortColumn === 'state.name' && header.sortable != false ? 'visible' : 'hidden'}"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</div>
</li>
</ul>
</span>
<!-- search field -->
<span class="form-group">
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.search" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isDeleteMode">
</span>
</span>
</span>
<!-- show all selected multiselectoptions -->
<span>
<span ng-repeat="state in states" class="pointer spacer-left-lg"
ng-if="!state.workflowHeader && multiselectFilter.state.indexOf(state.id) > -1"
ng-click="operateMultiselectFilter('state', state.id)"
ng-class="{'disabled': isDeleteMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ state.name | translate }}
</span>
</span>
<span ng-repeat="category in categories" class="pointer spacer-left-lg"
ng-if="multiselectFilter.category.indexOf(category.id) > -1"
ng-click="operateMultiselectFilter('category', category.id)"
ng-class="{'disabled': isDeleteMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ category.name }}
</span>
</span>
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"
ng-click="operateMultiselectFilter('tag', tag.id)"
ng-class="{'disabled': isDeleteMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ tag.name }} {{ tag.name }}
</span> </span>
<div ng-if="motion.origin"> </span>
<small> </span>
<i class="fa fa-info-circle"></i> <translate>Origin</translate>: </div>
{{ motion.origin | limitTo:30 }}{{ motion.origin.length > 30 ? '...' : '' }} </div>
</small>
</div>
<div ng-if="motion.isAllowed('update')" class="hoverActions" ng-class="{'hiddenDiv': !motion.hover}">
<span ng-if="motion.isAllowed('update')">
<a href="" ng-click="openDialog(motion)" translate>Edit</a>
</span>
<span ng-if="motion.isAllowed('quickedit')">
| <a href="" ng-click="motion.quickEdit=true" translate>QuickEdit</a> |
</span>
<span ng-if="motion.isAllowed('delete')">
<a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ motion.getTitle() }}</b>"
ng-bootbox-confirm-action="delete(motion)" translate>Delete</a>
</span>
</div>
<!-- submitters --> <!-- main table -->
<td ng-if="!motion.quickEdit" class="optional"> <!-- data row -->
<div ng-repeat="submitter in motion.submitters"> <div class="row data-row" ng-mouseover="motion.hover=true"
{{ submitter.get_full_name() }}<br> ng-mouseleave="motion.hover=false"
ng-class="{'projected': motion.isProjected()}"
ng-repeat="motion in motionsFiltered = (motions
| osFilter: filter.search : getFilterString
| SelectMultipleFilter: multiselectFilter.state : getItemId.state
| SelectMultipleFilter: multiselectFilter.category : getItemId.category
| SelectMultipleFilter: multiselectFilter.tag : getItemId.tag
| orderBy: sortColumn : reverse)">
<!-- select column -->
<div ng-show="isDeleteMode" os-perms="motions.can_manage"
class="col-xs-1 centered" ng-class="{'deleteColumn' : motion.selected}">
<i class="fa text-danger pointer" ng-click="motion.selected=!motion.selected"
ng-class="motion.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
</div>
<!-- projector column -->
<div class="col-xs-1 centered" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': motion.isProjected() }"
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
</div>
<!-- main content column -->
<div class="col-xs-6 content">
<div class="identifier-col">
<div class="nobr" ng-show="motion.identifier">
{{ motion.identifier }}:
</div>
<!-- hover menu -->
<div ng-if="motion.isAllowed('update')" ng-class="{'hiddenDiv': !motion.hover}">
<span ng-if="motion.isAllowed('update')">
<a href="" ng-click="openDialog(motion)">
<i class="fa fa-pencil"></i></a>
</span>
<span ng-if="motion.isAllowed('delete')">
&nbsp;<a href="" ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ motion.getTitle() }}</b>"
ng-bootbox-confirm-action="delete(motion)">
<i class="fa fa-trash text-danger"></i></a>
</span>
</div>
</div>
<div class="title-col">
<!-- ID and title -->
<div>
<strong>
<a class="title" ui-sref="motions.motion.detail({id: motion.id})">{{ motion.getTitle() }}</a>
</strong>
<i class="fa fa-paperclip" ng-if="motion.attachments_id.length > 0"></i>
<span style="padding: 5px;" ng-mouseover="motion.stateHover=true" ng-mouseleave="motion.stateHover=false">
<span class="label" ng-class="'label-'+motion.state.css_class">
{{ motion.state.name | translate }}
</span>
<span ng-class="{'hiddenDiv': !motion.stateHover}" uib-dropdown>
<i class="fa fa-cog pointer" uib-dropdown-toggle id="state-dropdown{{ motion.id }}"></i>
<ul uib-dropdown-menu aria-labelledby="state-dropdown{{ motion.id }}">
<li ng-repeat="state in motion.state.getNextStates()">
<a href ng-click="updateState(motion, state.id)">{{ state.action_word | translate }}</a>
</li>
<li class="divider" ng-if="motion.state.getNextStates().length"></li>
<li>
<a href ng-if="motion.isAllowed('reset_state')" ng-click="reset_state(motion)">
<i class="fa fa-exclamation-triangle"></i>
<translate>Reset state</translate>
</a>
</li>
</ul>
</span>
</span>
</div>
<!-- Submitters -->
<div>
<span>
<span class="optional" translate>by</span>
<span class="optional" ng-repeat="submitter in motion.submitters | limitTo:3">
{{ submitter.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 3">, ...</span>
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
</span>
</div> </div>
<!-- category --> </div>
<td ng-if="!motion.quickEdit" class="optional"> </div>
{{ motion.category.name }} <!-- additional content column -->
<style>
#motion-table .row .col-xs-4 {
width: calc(50% - {{ isDeleteMode ? '100' : '50' }}px);
}
</style>
<div class="col-xs-4 content">
<div style="width: 60%;" class="optional">
<small>
<div ng-mouseover="motion.categoryHover=true"
ng-mouseleave="motion.categoryHover=false"
ng-show="categories.length > 0">
<!-- Category dropdown -->
<span uib-dropdown>
<span id="dropdown-category{{ motion.id }}" class="pointer"
uib-dropdown-toggle uib-tooltip="{{ 'Set a category' | translate }}"
tooltip-class="nobr">
<span ng-if="motion.category == null" ng-show="motion.hover">
<i class="fa fa-sitemap"></i>
<i class="fa fa-plus"></i>
</span>
<span ng-if="motion.category != null">
<i class="fa fa-sitemap spacer-right"></i>
{{ motion.category.name }}
<i class="fa fa-cog fa-lg spacer-left" ng-show="motion.categoryHover"></i>
</span>
</span>
<ul class="dropdown-menu" aria-labelledby="dropdown-category{{ motion.id }}">
<li ng-repeat="category in categories">
<div class="dropdown-entry pointer"
ng-click="toggle_category(motion, category)">
<i class="fa fa-check" ng-if="category.id == motion.category.id"></i>
{{ category.name }}
</div>
</li>
</ul>
</span>
</div>
<div ng-mouseover="motion.tagHover=true"
ng-mouseleave="motion.tagHover=false"
ng-show="tags.length > 0">
<span uib-dropdown>
<span id="dropdown-tags{{ motion.id }}" class="pointer"
uib-dropdown-toggle uib-tooltip="{{ 'Add a tag' | translate }}"
tooltip-class="nobr">
<span ng-if="motion.tags.length == 0" ng-show="motion.hover">
<i class="fa fa-tags"></i>
<i class="fa fa-plus"></i>
</span>
<span ng-if="motion.tags.length > 0">
<i class="fa fa-tags spacer-right"></i>
<span ng-repeat="tag in motion.tags">
{{ tag.name }}<span ng-if="!$last">,</span>
</span>
<i class="fa fa-cog fa-lg spacer-left" ng-show="motion.tagHover"></i>
</span>
</span>
<ul class="dropdown-menu" aria-labelledby="dropdown-tags{{ motion.id }}">
<li ng-repeat="tag in tags">
<div class="dropdown-entry pointer" ng-click="toggle_tag(motion, tag)">
<i class="fa fa-check" ng-if="has_tag(motion, tag)"></i>
{{ tag.name }}
</div>
</li>
</ul>
</span>
</div>
<div ng-if="motion.origin">
<i class="fa fa-share spacer-right" uib-tooltip="{{ 'Origin' | translate }}"></i>
{{ motion.origin | limitTo:25 }}{{ motion.origin.length > 25 ? '...' : '' }}
</div>
</small>
</div>
<div style="width: 10%;" class="pull-right optional">
<div class="pull-right" ng-if="config('motions_min_supporters') != 0"
uib-tooltip="{{ motion.supporters.length }} {{ 'Supporters' | translate }}
{{ (config('motions_min_supporters') - motion.supporters.length) > 0 ? '(' + (config('motions_min_supporters') - motion.supporters.length) + ' ' + ('needed' | translate) + ')': '' }}"
tooltip-class="nobr">
<span class="badge"
ng-class="{'badge-info': motion.supporters.length < config('motions_min_supporters')}">
{{ motion.supporters.length }}
</span>
</div>
</div>
<div style="width: 30%;" class="pull-right">
<div class="centered">{{ motion.agenda_item.getItemNumberWithAncestors() }}</div>
</div>
</div>
</div> <!-- data row -->
<!-- state --> </div> <!-- container -->
<td ng-if="!motion.quickEdit" class="optional"> </div> <!-- details -->
<span class="label" ng-class="'label-'+motion.state.css_class">
{{ motion.state.name | translate }}
</span>
<!-- quickEdit columns -->
<td ng-if="motion.quickEdit && motion.isAllowed('quickedit')" class="quickmode" colspan="6">
<h4>{{ motion.getTitle() }} <span class="text-muted">&ndash; <translate>QuickEdit</translate></span></h4>
<uib-alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
{{ alert.msg }}
</uib-alert>
<div class="row">
<div class="col-xs-6">
<label for="inputIdentifier" translate>Identifier</label>
<input type="text" ng-model="motion.identifier" class="form-control input-sm"
id="inputIdentifier">
</div>
<div class="col-xs-6">
<label for="selectCategory" translate>Category</label>
<select ng-options="category.id as category.name for category in categories"
ng-model="motion.category_id" class="form-control" id="selectCategory">
</select>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<label for="selectSubmitter" translate>Submitters</label>
<select multiple chosen
ng-model="motion.submitters_id"
ng-options="user.id as user.full_name for user in users"
search-contains="true"
id="selectSubmitter"
class="form-control"
data-placeholder-text-multiple="'Select or search a submitter ...' | translate"
no-results-text="'No results match' | translate">
</select>
</div>
<div class="col-xs-6">
<label for="selectTags" translate>Tags</label>
<select multiple chosen
ng-model="motion.tags_id"
ng-options="tag.id as tag.name for tag in tags"
search-contains="true"
id="selectTag"
class="form-control"
data-placeholder-text-multiple="'Select or search a tag ...' | translate"
no-results-text="'No results match' | translate">
</select>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<div ng-if="config('motions_min_supporters') > 0">
<label for="selectSupporter" translate>Supporters</label>
<select multiple chosen
ng-model="motion.supporters_id"
ng-options="user.id as user.full_name for user in users"
search-contains="true"
id="selectSupporter"
class="form-control"
data-placeholder-text-multiple="'Select or search a supporter ...' | translate"
no-results-text="'No results match' | translate">
<option value=""></option>
</select>
</div>
</div>
</div>
<div class="spacer">
<button ng-click="cancelQuickEdit(motion)" class="btn btn-default pull-left" translate>
Cancel
</button> &nbsp;
<button ng-if="motion.isAllowed('update')" ng-click="save(motion)" class="btn btn-primary" translate>
Update
</button>
<a ng-if="motion.isAllowed('update')" ui-sref="motions.motion.detail.update({id: motion.id })"
class="pull-right" translate>Edit motion ...</a>
</div>
</table>
</div>