Merge branch 'dev-angular' into feature/client-side-routing

Signed-off-by: Alex Coles <alex@alexbcoles.com>

Conflicts:
	app/assets/javascripts/angular/openproject-app.js
	app/views/work_packages/_list.html.erb
pull/1315/head
Alex Coles 11 years ago
commit f5f98e81de
  1. 62
      app/assets/javascripts/angular/controllers/dialogs/columns.js
  2. 6
      app/assets/javascripts/angular/controllers/work-packages-controller.js
  3. 70
      app/assets/javascripts/angular/directives/work_packages/column-context-menu-directive.js
  4. 8
      app/assets/javascripts/angular/directives/work_packages/options-dropdown-directive.js
  5. 26
      app/assets/javascripts/angular/directives/work_packages/query-columns-directive.js
  6. 27
      app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js
  7. 19
      app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js
  8. 30
      app/assets/javascripts/angular/helpers/work-packages-table-helper.js
  9. 4
      app/assets/javascripts/angular/models/query.js
  10. 2
      app/assets/javascripts/angular/models/timelines/mixins/ui.js
  11. 1
      app/assets/javascripts/angular/openproject-app.js
  12. 69
      app/assets/javascripts/angular/services/query-service.js
  13. 28
      app/assets/javascripts/angular/services/work-packages-table-service.js
  14. 1
      app/assets/javascripts/application.js.erb
  15. 50
      app/assets/stylesheets/content/_action_menu_main.md
  16. 46
      app/assets/stylesheets/content/_action_menu_main.sass
  17. 101
      app/assets/stylesheets/content/_context_menu.sass
  18. 2
      app/assets/stylesheets/content/_modal.sass
  19. 2
      app/assets/stylesheets/context_menu_rtl.css
  20. 20
      app/assets/stylesheets/fonts/_openproject_icon_font.sass
  21. 1
      app/assets/stylesheets/global/_variables.sass
  22. 8
      app/assets/stylesheets/select2_customizing.css.erb
  23. 7
      app/controllers/statuses_controller.rb
  24. 9
      app/views/my/page_layout.html.erb
  25. 3
      bower.json
  26. 1
      config/locales/de.yml
  27. 1
      config/locales/en.yml
  28. 4
      config/locales/js-de.yml
  29. 6
      config/locales/js-en.yml
  30. 5
      doc/CHANGELOG.md
  31. 2
      features/project_types/project_creation_with_type.feature
  32. 11
      features/session/user_session.feature
  33. 11
      features/step_definitions/my_page_steps.rb
  34. 4
      features/step_definitions/web_steps.rb
  35. 6
      features/work_packages/bulk.feature
  36. 2
      features/work_packages/moves/work_package_moves_new_copy.feature
  37. 2
      karma.conf.js
  38. 6
      karma/tests/controllers/work-packages-controller-test.js
  39. 137
      karma/tests/directives/work_packages/column-context-menu-directive-test.js
  40. 7
      karma/tests/directives/work_packages/work-package-context-menu-directive-test.js
  41. 3
      karma/tests/helpers/work-package-context-menu-helper-test.js
  42. 17
      karma/tests/helpers/work-package-table-helper-test.js
  43. 2
      lib/redmine/menu_manager/top_menu_helper.rb
  44. 6
      public/templates/timelines/toolbar.html
  45. 1
      public/templates/work_packages.list.html
  46. 28
      public/templates/work_packages/column_context_menu.html
  47. 16
      public/templates/work_packages/modals/columns.html
  48. 6
      public/templates/work_packages/query_columns.html
  49. 1
      public/templates/work_packages/sort_header.html
  50. 2
      public/templates/work_packages/work_package_context_menu.html
  51. 10
      public/templates/work_packages/work_packages_table.html
  52. 17
      spec/controllers/statuses_controller_spec.rb
  53. 2
      spec/features/accessibility/work_packages/work_package_query_spec.rb
  54. 40
      spec/views/layouts/base.html.erb_spec.rb

@ -36,7 +36,67 @@ angular.module('openproject.workPackages.controllers')
}); });
}]) }])
.controller('ColumnsModalController', ['columnsModal', function(columnsModal) { .controller('ColumnsModalController', ['$scope',
'$timeout',
'$filter',
'columnsModal',
'QueryService',
'WorkPackageService',
'WorkPackagesTableService',
function($scope, $timeout, $filter, columnsModal, QueryService, WorkPackageService, WorkPackagesTableService) {
this.name = 'Columns'; this.name = 'Columns';
this.closeMe = columnsModal.deactivate; this.closeMe = columnsModal.deactivate;
$scope.getObjectsData = function(term, result) {
// Note: This relies on the columns having been cached in the service so they can be instantly available.
result($filter('filter')($scope.availableColumnsData, {label: term}));
};
// Data conversion for select2
function convertColumnsForSelect2(columns) {
return columns.map(function(column){
return { id: column.name, label: column.title, other: column.title };
})
}
function getColumnIdentifiersFromSelection(selectedColumnsData) {
return selectedColumnsData.map(function(column) { return column.id; })
}
// Selected Columns
var selectedColumns = QueryService.getSelectedColumns();
var previouslySelectedColumnNames = selectedColumns
.map(function(column){ return column.name; });
function getNewlyAddedColumns() {
return selectedColumns.select(function(column){
return previouslySelectedColumnNames.indexOf(column.name) < 0;
});
}
$scope.selectedColumnsData = convertColumnsForSelect2(selectedColumns);
// Available selectable Columns
QueryService.getAvailableUnusedColumns()
.then(function(availableUnusedColumns){
$scope.availableUnusedColumns = availableUnusedColumns;
$scope.availableColumnsData = convertColumnsForSelect2(availableUnusedColumns);
});
$scope.updateSelectedColumns = function(){
// Note: Can't directly manipulate selected columns because select2 returns a new array when you change the values:(
QueryService.setSelectedColumns(getColumnIdentifiersFromSelection($scope.selectedColumnsData));
// Augment work packages with new columns data
var addedColumns = getNewlyAddedColumns(),
currentWorkPackages = WorkPackagesTableService.getRowsData(),
groupBy = WorkPackagesTableService.getGroupBy();
if(groupBy.length === 0) groupBy = undefined; // don't pass an empty string as groupBy
if(addedColumns.length) WorkPackageService.augmentWorkPackagesWithColumnsData(currentWorkPackages, addedColumns, groupBy);
columnsModal.deactivate();
}
}]); }]);

@ -116,6 +116,7 @@ angular.module('openproject.workPackages.controllers')
WorkPackagesTableService.setColumns($scope.query.columns); WorkPackagesTableService.setColumns($scope.query.columns);
WorkPackagesTableService.addColumnMetaData(meta); WorkPackagesTableService.addColumnMetaData(meta);
WorkPackagesTableService.setRows(WorkPackagesTableHelper.getRows(workPackages, $scope.query.groupBy)); WorkPackagesTableService.setRows(WorkPackagesTableHelper.getRows(workPackages, $scope.query.groupBy));
WorkPackagesTableService.setGroupBy($scope.query.groupBy);
WorkPackagesTableService.setBulkLinks(bulkLinks); WorkPackagesTableService.setBulkLinks(bulkLinks);
// query data // query data
@ -139,10 +140,9 @@ angular.module('openproject.workPackages.controllers')
} }
function initAvailableColumns() { function initAvailableColumns() {
return QueryService.getAvailableColumns($scope.projectIdentifier) return QueryService.getAvailableUnusedColumns($scope.projectIdentifier)
.then(function(data){ .then(function(data){
$scope.availableColumns = WorkPackagesTableHelper.getColumnDifference(data.available_columns, $scope.columns); $scope.availableUnusedColumns = data;
return $scope.availableColumns;
}); });
} }

@ -0,0 +1,70 @@
angular.module('openproject.workPackages.directives')
.directive('columnContextMenu', [
'ContextMenuService',
'I18n',
'QueryService',
'WorkPackagesTableHelper',
'WorkPackagesTableService',
function(ContextMenuService, I18n, QueryService, WorkPackagesTableHelper, WorkPackagesTableService) {
return {
restrict: 'EA',
replace: true,
scope: {},
templateUrl: '/templates/work_packages/column_context_menu.html',
link: function(scope, element, attrs) {
var contextMenuName = 'columnContextMenu';
// Wire up context menu handlers
ContextMenuService.registerMenuElement(contextMenuName, element);
scope.contextMenu = ContextMenuService.getContextMenu();
scope.$watch('contextMenu.opened', function(opened) {
scope.opened = opened && scope.contextMenu.targetMenu === contextMenuName;
});
scope.$watch('contextMenu.targetMenu', function(target) {
scope.opened = scope.contextMenu.opened && target === contextMenuName;
});
// shared context information
scope.$watch('contextMenu.context.column', function(column) {
scope.column = column;
});
scope.$watch('contextMenu.context.columns', function(columns) {
scope.columns = columns;
});
scope.I18n = I18n;
// context menu actions
scope.groupBy = function(columnName) {
QueryService.getQuery().groupBy = columnName;
};
scope.sortAscending = function(columnName) {
WorkPackagesTableService.sortBy(columnName, 'asc');
};
scope.sortDescending = function(columnName) {
WorkPackagesTableService.sortBy(columnName, 'desc');
};
scope.moveLeft = function(columnName) {
WorkPackagesTableHelper.moveColumnBy(scope.columns, columnName, -1);
};
scope.moveRight = function(columnName) {
WorkPackagesTableHelper.moveColumnBy(scope.columns, columnName, 1);
};
scope.hideColumn = function(columnName) {
QueryService.hideColumns(new Array(columnName));
};
}
};
}]);

@ -61,9 +61,15 @@ angular.module('openproject.workPackages.directives')
} }
}; };
scope.showColumnsModal = function(){
scope.$emit('hideAllDropdowns');
columnsModal.activate();
};
scope.toggleDisplaySums = function(){ scope.toggleDisplaySums = function(){
scope.$emit('hideAllDropdowns');
scope.query.displaySums = !scope.query.displaySums; scope.query.displaySums = !scope.query.displaySums;
} };
} }
}; };
}]); }]);

@ -28,7 +28,11 @@
angular.module('openproject.workPackages.directives') angular.module('openproject.workPackages.directives')
.directive('queryColumns', ['WorkPackagesTableHelper', 'WorkPackageService', function(WorkPackagesTableHelper, WorkPackageService) { .directive('queryColumns', [
'WorkPackagesTableHelper',
'WorkPackageService',
'QueryService',
function(WorkPackagesTableHelper, WorkPackageService, QueryService) {
return { return {
restrict: 'E', restrict: 'E',
@ -37,14 +41,14 @@ angular.module('openproject.workPackages.directives')
compile: function(tElement) { compile: function(tElement) {
return { return {
pre: function(scope) { pre: function(scope) {
scope.moveColumns = function (columnNames, fromColumns, toColumns, requires_extension) { scope.showColumns = function(columnNames) {
angular.forEach(columnNames, function(columnName){ QueryService.showColumns(columnNames);
removeColumn(columnName, fromColumns, function(removedColumn){
toColumns.push(removedColumn); extendRowsWithColumnData(columnNames); // TODO move to QueryService
}); };
});
if (requires_extension) extendRowsWithColumnData(columnNames); scope.hideColumns = function(columnNames) {
QueryService.hideColumns(columnNames);
}; };
scope.moveSelectedColumnBy = function(by) { scope.moveSelectedColumnBy = function(by) {
@ -52,6 +56,7 @@ angular.module('openproject.workPackages.directives')
WorkPackagesTableHelper.moveColumnBy(scope.columns, nameOfColumnToBeMoved, by); WorkPackagesTableHelper.moveColumnBy(scope.columns, nameOfColumnToBeMoved, by);
}; };
// TODO move to WorkPackagesService
function extendRowsWithColumnData(columnNames) { function extendRowsWithColumnData(columnNames) {
var workPackages = scope.rows.map(function(row) { var workPackages = scope.rows.map(function(row) {
return row.object; return row.object;
@ -64,11 +69,6 @@ angular.module('openproject.workPackages.directives')
scope.withLoading(WorkPackageService.augmentWorkPackagesWithColumnsData, params) scope.withLoading(WorkPackageService.augmentWorkPackagesWithColumnsData, params)
.then(scope.updateBackUrl); .then(scope.updateBackUrl);
} }
function removeColumn(columnName, columns, callback) {
var removed = columns.splice(WorkPackagesTableHelper.getColumnIndexByName(columns, columnName), 1).first();
return !(typeof(callback) === 'undefined') ? callback.call(this, removed) : null;
}
} }
}; };
} }

@ -33,6 +33,7 @@ angular.module('openproject.workPackages.directives')
return { return {
restrict: 'A', restrict: 'A',
templateUrl: '/templates/work_packages/sort_header.html', templateUrl: '/templates/work_packages/sort_header.html',
transclude: true,
scope: { scope: {
query: '=', query: '=',
headerName: '=', headerName: '=',
@ -41,29 +42,35 @@ angular.module('openproject.workPackages.directives')
locale: '=' locale: '='
}, },
link: function(scope, element, attributes) { link: function(scope, element, attributes) {
scope.$watch('query.sortation.sortElements', function(newValue, oldValue){ scope.$watch('query.sortation.sortElements', function(sortElements){
if (scope.headerName != newValue[0].field) scope.currentSortDirection = null; var latestSortElement = sortElements[0];
if (scope.headerName !== latestSortElement.field) {
scope.currentSortDirection = null;
} else {
scope.currentSortDirection = latestSortElement.direction;
}
setFullTitle();
}, true); }, true);
scope.performSort = function(){ scope.performSort = function(){
targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName); var targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName);
scope.query.setSortation(targetSortation); scope.query.setSortation(targetSortation);
scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName); scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName);
scope.setFullTitle();
}; };
scope.setFullTitle = function(){ function setFullTitle() {
if(!scope.sortable) scope.fullTitle = ''; if(!scope.sortable) scope.fullTitle = '';
if(scope.currentSortDirection){
if(scope.currentSortDirection) {
var sortDirectionText = (scope.currentSortDirection == 'asc') ? I18n.t('js.label_ascending') : I18n.t('js.label_descending'); var sortDirectionText = (scope.currentSortDirection == 'asc') ? I18n.t('js.label_ascending') : I18n.t('js.label_descending');
scope.fullTitle = sortDirectionText + " " + I18n.t('js.label_sorted_by') + ' \"' + scope.headerTitle + '\"'; scope.fullTitle = sortDirectionText + " " + I18n.t('js.label_sorted_by') + ' \"' + scope.headerTitle + '\"';
} else { } else {
scope.fullTitle = (I18n.t('js.label_sort_by') + ' \"' + scope.headerTitle + '\"'); scope.fullTitle = (I18n.t('js.label_sort_by') + ' \"' + scope.headerTitle + '\"');
} }
}; }
scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName);
scope.setFullTitle();
} }
}; };
}]); }]);

@ -15,20 +15,25 @@ angular.module('openproject.workPackages.directives')
scope: {}, scope: {},
templateUrl: '/templates/work_packages/work_package_context_menu.html', templateUrl: '/templates/work_packages/work_package_context_menu.html',
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
var contextMenuName = 'workPackageContextMenu';
scope.I18n = I18n; scope.I18n = I18n;
scope.opened = false;
// wire up context menu event handler // wire up context menu event handler
ContextMenuService.registerMenuElement(contextMenuName, element);
ContextMenuService.setTarget(element);
scope.contextMenu = ContextMenuService.getContextMenu(); scope.contextMenu = ContextMenuService.getContextMenu();
scope.$watch('contextMenu.opened', function(opened) { scope.$watch('contextMenu.opened', function(opened) {
scope.opened = opened; scope.opened = opened && scope.contextMenu.targetMenu === contextMenuName;
});
scope.$watch('contextMenu.targetMenu', function(target) {
scope.opened = scope.contextMenu.opened && target === contextMenuName;
}); });
scope.$watch('contextMenu.context.row', function() { scope.$watch('contextMenu.context.row', function(row) {
updateContextMenu(getWorkPackagesFromContext(scope.contextMenu.context)); if (row && scope.contextMenu.targetMenu === contextMenuName) {
updateContextMenu(getWorkPackagesFromContext(scope.contextMenu.context));
}
}); });
scope.triggerContextMenuAction = function(action, link) { scope.triggerContextMenuAction = function(action, link) {
@ -53,8 +58,6 @@ angular.module('openproject.workPackages.directives')
}); });
WorkPackagesTableService.removeRows(rows); WorkPackagesTableService.removeRows(rows);
// TODO remove via a controller linked to the work packages row
// Remark: This controller has to be forwarded by the hasContextMenu directive somehow
}) })
.error(function(data, status) { .error(function(data, status) {
// TODO wire up to API and processs API response // TODO wire up to API and processs API response

@ -89,16 +89,16 @@ angular.module('openproject.workPackages.helpers')
}, },
getColumnDifference: function (allColumns, columns) { getColumnDifference: function (allColumns, columns) {
var columnValues = columns.map(function(column){ var identifiers = columns.map(function(column){
return column.name; return column.name;
}); });
return this.getColumnDifferenceByName(allColumns, columnValues); return this.getColumnDifferenceByName(allColumns, identifiers);
}, },
getColumnDifferenceByName: function (allColumns, columnValues) { getColumnDifferenceByName: function (allColumns, identifiers) {
return allColumns.filter(function(column) { return allColumns.filter(function(column) {
return columnValues.indexOf(column.name) === -1; return identifiers.indexOf(column.name) === -1;
}); });
}, },
@ -126,12 +126,18 @@ angular.module('openproject.workPackages.helpers')
}); });
}, },
mapIdentifiersToColumns: function(columns, columnNames) {
return columnNames.map(function(columnName) {
return WorkPackagesTableHelper.detectColumnByName(columns, columnName);
});
},
moveElementBy: function(array, index, positions) { moveElementBy: function(array, index, positions) {
// TODO maybe extend the Array prototype // TODO maybe extend the Array prototype
var newPosition = index + positions; var newPosition = index + positions;
if (newPosition > -1 && newPosition < array.length) { if (newPosition > -1 && newPosition < array.length) {
var elementToMove = array.splice(index, 1).first(); var elementToMove = array.splice(index, 1)[0];
array.splice(newPosition, 0, elementToMove); array.splice(newPosition, 0, elementToMove);
} }
}, },
@ -142,6 +148,20 @@ angular.module('openproject.workPackages.helpers')
WorkPackagesTableHelper.moveElementBy(columns, index, by); WorkPackagesTableHelper.moveElementBy(columns, index, by);
}, },
moveColumns: function (columnNames, fromColumns, toColumns) {
angular.forEach(columnNames, function(columnName){
WorkPackagesTableHelper.removeColumn(columnName, fromColumns, function(removedColumn){
toColumns.push(removedColumn);
});
});
},
removeColumn: function(columnName, columns, callback) {
var removed = columns.splice(this.getColumnIndexByName(columns, columnName), 1)[0];
return typeof(callback) !== 'undefined' ? callback.call(this, removed) : null;
},
getSelectedRows: function(rows) { getSelectedRows: function(rows) {
return rows return rows
.filter(function(row) { .filter(function(row) {

@ -162,6 +162,10 @@ angular.module('openproject.models')
}); });
}, },
getSelectedColumns: function(){
return this.columns;
},
getParamColumns: function(){ getParamColumns: function(){
var selectedColumns = this.columns.map(function(column) { var selectedColumns = this.columns.map(function(column) {
return column.name; return column.name;

@ -819,7 +819,7 @@ angular.module('openproject.timelines.models')
}) })
).attr({ ).attr({
'stroke': 'red', 'stroke': 'red',
'stroke-dasharray': '- ' 'stroke-dasharray': '3,3'
}); });
var setDateTime = 5 * 60 * 1000; var setDateTime = 5 * 60 * 1000;

@ -104,6 +104,7 @@ angular.module('openproject.timeEntries.controllers', []);
// main app // main app
var openprojectApp = angular.module('openproject', [ var openprojectApp = angular.module('openproject', [
'ui.select2', 'ui.select2',
'ui.select2.sortable',
'ui.date', 'ui.date',
'ui.router', 'ui.router',
'openproject.uiComponents', 'openproject.uiComponents',

@ -28,12 +28,32 @@
angular.module('openproject.services') angular.module('openproject.services')
.service('QueryService', ['Query', 'Sortation', '$http', '$location', 'PathHelper', '$q', 'AVAILABLE_WORK_PACKAGE_FILTERS', 'StatusService', 'TypeService', 'PriorityService', 'UserService', 'VersionService', 'RoleService', 'GroupService', 'ProjectService', 'I18n', .service('QueryService', [
function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService, I18n) { 'Query',
'Sortation',
'$http',
'$location',
'PathHelper',
'$q',
'AVAILABLE_WORK_PACKAGE_FILTERS',
'StatusService',
'TypeService',
'PriorityService',
'UserService',
'VersionService',
'RoleService',
'GroupService',
'ProjectService',
'WorkPackagesTableHelper',
'I18n',
function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService, WorkPackagesTableHelper, I18n) {
var query; var query;
var availableColumns = [], availableFilterValues = {}, availableFilters = {}; var availableColumns = [],
availableUnusedColumns = [],
availableFilterValues = {},
availableFilters = {};
var totalEntries; var totalEntries;
@ -79,6 +99,14 @@ angular.module('openproject.services')
return totalEntries; return totalEntries;
}, },
hideColumns: function(columnNames) {
WorkPackagesTableHelper.moveColumns(columnNames, this.getSelectedColumns(), availableColumns);
},
showColumns: function(columnNames) {
WorkPackagesTableHelper.moveColumns(columnNames, availableColumns, this.getSelectedColumns());
},
// data loading // data loading
getAvailableGroupedQueries: function(projectIdentifier) { getAvailableGroupedQueries: function(projectIdentifier) {
@ -87,10 +115,37 @@ angular.module('openproject.services')
return QueryService.doQuery(url); return QueryService.doQuery(url);
}, },
getAvailableUnusedColumns: function(projectIdentifier) {
return QueryService.getAvailableColumns(projectIdentifier)
.then(function(available_columns) {
availableUnusedColumns = WorkPackagesTableHelper.getColumnDifference(available_columns, QueryService.getSelectedColumns());
return availableUnusedColumns;
});
},
getAvailableColumns: function(projectIdentifier) { getAvailableColumns: function(projectIdentifier) {
// TODO: Once we have a single page app we need to differentiate between different project columns
if(availableColumns.length) {
return $q.when(availableColumns);
}
var url = projectIdentifier ? PathHelper.apiProjectAvailableColumnsPath(projectIdentifier) : PathHelper.apiAvailableColumnsPath(); var url = projectIdentifier ? PathHelper.apiProjectAvailableColumnsPath(projectIdentifier) : PathHelper.apiAvailableColumnsPath();
return QueryService.doQuery(url); return QueryService.doGet(url, function(response){
availableColumns = response.data.available_columns;
return availableColumns;
});
},
getSelectedColumns: function() {
return this.getQuery().getSelectedColumns();
},
setSelectedColumns: function(selectedColumnNames) {
var currentColumns = this.getSelectedColumns();
this.hideColumns(currentColumns.map(function(column) { return column.name; }));
this.showColumns(selectedColumnNames);
}, },
getAvailableFilters: function(projectIdentifier){ getAvailableFilters: function(projectIdentifier){
@ -193,6 +248,8 @@ angular.module('openproject.services')
return availableFilters[projectIdentifier]; return availableFilters[projectIdentifier];
}, },
// synchronization
saveQuery: function() { saveQuery: function() {
var url = PathHelper.apiProjectQueryPath(query.project_id, query.id); var url = PathHelper.apiProjectQueryPath(query.project_id, query.id);
return QueryService.doQuery(url, query.toUpdateParams(), 'PUT', function(response){ return QueryService.doQuery(url, query.toUpdateParams(), 'PUT', function(response){
@ -209,6 +266,10 @@ angular.module('openproject.services')
}); });
}, },
doGet: function(url, success, failure) {
return QueryService.doQuery(url, null, 'GET', success, failure);
},
doQuery: function(url, params, method, success, failure) { doQuery: function(url, params, method, success, failure) {
method = method || 'GET'; method = method || 'GET';
success = success || function(response){ success = success || function(response){

@ -28,7 +28,10 @@
angular.module('openproject.workPackages.services') angular.module('openproject.workPackages.services')
.service('WorkPackagesTableService', ['$filter', function($filter) { .service('WorkPackagesTableService', [
'$filter',
'QueryService',
function($filter, QueryService) {
var workPackagesTableData = { var workPackagesTableData = {
allRowsChecked: false allRowsChecked: false
}; };
@ -77,10 +80,25 @@ angular.module('openproject.workPackages.services')
setRows: function(rows) { setRows: function(rows) {
workPackagesTableData.rows = rows; workPackagesTableData.rows = rows;
}, },
getRows: function() { getRows: function() {
return workPackagesTableData.rows; return workPackagesTableData.rows;
}, },
getRowsData: function() {
return WorkPackagesTableService.getRows().map(function(row) {
return row.object;
});
},
getGroupBy: function() {
return workPackagesTableData.groupBy;
},
setGroupBy: function(groupBy) {
workPackagesTableData.groupBy = groupBy;
},
removeRow: function(row) { removeRow: function(row) {
var rows = workPackagesTableData.rows; var rows = workPackagesTableData.rows;
var index = rows.indexOf(row); var index = rows.indexOf(row);
@ -91,8 +109,14 @@ angular.module('openproject.workPackages.services')
angular.forEach(rows, function(row) { angular.forEach(rows, function(row) {
WorkPackagesTableService.removeRow(row); WorkPackagesTableService.removeRow(row);
}); });
} },
sortBy: function(columnName, direction) {
QueryService.getQuery().sortation.addSortElement({
field: columnName,
direction: direction
});
}
}; };
return WorkPackagesTableService; return WorkPackagesTableService;

@ -58,6 +58,7 @@
//= require angular-modal //= require angular-modal
//= require angular-ui-router //= require angular-ui-router
//= require angular-ui-select2 //= require angular-ui-select2
//= require angular-ui-select2-sortable
//= require angular-ui-date/src/date //= require angular-ui-date/src/date
//= require angular-sanitize //= require angular-sanitize

@ -1 +1,51 @@
# Action menu # Action menu
```
<div class="action-menu">
<ul class="menu">
<li>
<a href="#"><i class="icon-edit icon-actionmenu"></i>menu item for modal...</a>
</li>
<li>
<a href="#"><i class="icon-yes icon-actionmenu"></i>menu item</a>
</li>
<li>
<a href="#"><i class="icon-copy icon-actionmenu"></i>menu item</a>
</li>
<li class="submenu-item">
<a href="#"><i class="icon-priority icon-actionmenu"></i>menu item with sub</a><i class="icon-pulldown-arrow4 icon-submenu"></i>
</li>
<li>
<a href="#"><i class="icon-delete icon-actionmenu"></i>menu item</a>
</li>
<li class="hasnoicon">
<a href="#">menu item no icon</a>
</li>
<li class="dropdown-divider"></li>
<li>
<a href="#"><i class="icon-time icon-actionmenu"></i>menu item</a>
</li>
</ul>
</div>
<div id="submenu" class="action-menu">
<ul class="menu">
<li>
<a href="#"><i class="icon-edit icon-actionmenu"></i>menu item for modal...</a>
</li>
<li>
<a href="#"><i class="icon-yes icon-actionmenu"></i>menu item</a>
</li>
<li>
<a href="#"><i class="icon-copy icon-actionmenu"></i>menu item</a>
</li>
</ul>
</div>
```

@ -82,3 +82,49 @@ ul.action_menu_more
.message-reply-menu .message-reply-menu
@include contextual(-39px) @include contextual(-39px)
.action-menu
float: left
width: 200px
border: 1px solid #dddddd
box-shadow: 1px 1px 4px #cccccc
-webkit-box-shadow: 1px 1px 4px #cccccc
padding: 3px 0
background: $action_menu_bg_color
ul
list-style-type: none
padding: 0
margin: 0
li
padding: 4px 10px
&:hover
background: #f0f0f0
&.hasnoicon
padding: 4px 10px 4px 35px
&.dropdown-divider
border-top: 1px solid #eeeeee
margin: 3px 0
padding: 0
font-size: 1px
a
color: $main_menu_font_color
font-weight: normal
white-space: nowrap
&:hover
text-decoration: none
i
.icon-actionmenu
padding: 0 10px 0 0
font-size: 15px
line-height: 5px
vertical-align: -40%
.icon-submenu
padding: 0 0 0 0
float: right
font-size: 15px
line-height: 5px
vertical-align: -40%
#submenu
margin: 81px 0 0 0

@ -34,106 +34,9 @@
padding: 0 padding: 0
border: 0 border: 0
#context-menu #work-package-context-menu, #column-context-menu
position: absolute &.action-menu
font-size: 0.9em
left: -7px
top: -7px
padding: 6px
z-index: 21
ul
@include context_menu_defaults
width: 140px
list-style: none
position: absolute position: absolute
left: -7px
z-index: 20
background: #f4f4f4
border: 1px solid #afafaf
li
@include context_menu_defaults
position: relative
padding: 1px
padding: 6px
z-index: 39
border: 1px solid white
background-position: 6px center
background-repeat: no-repeat
cursor: pointer
border-top: 1px solid #fff
border-bottom: 1px solid #ddd
> a
width:auto
&:hover
border: 1px solid gray
background-color: #eee
&.folder
&:hover
z-index: 40
div.submenu
background: url(image-path('arrow-right.png')) no-repeat right
position: absolute
height: 9px
width: 7px
top: 11px
right: 6px
ul
display: none
position: absolute
max-height: 400px
overflow-x: hidden
overflow-y: auto
left: 140px
top: -1px
width: auto
z-index: 19
a
@include context_menu_defaults
border: none
background-repeat: no-repeat
background-position: 1px 50%
padding: 1px 10px 1px 15px
width: 100%
&.icon, &.icon-context
color: $content_icon_link_hover_color
font-weight: bold
padding-left: 0
&.disabled, &.disabled:hover
color: #ccc
&:hover
color: #2A5685
border: none
text-decoration: none
&.reverse-y li.folder>ul
top: auto
bottom: 0
&.reverse-x li.folder
ul
left: auto
right: 168px
>ul
right: 148px
#context-menu ul ul, #context-menu li:hover ul ul
display: none
#context-menu li:hover ul, #context-menu li:hover li:hover ul
display: block
#context-menu li li
padding: 6px 12px
width: auto
display: block
white-space: nowrap
#context-menu li:hover ul
display: block
.hascontextmenu .hascontextmenu
cursor: context-menu cursor: context-menu

@ -14,7 +14,7 @@ $ng-modal-image-width: $ng-modal-image-height
+position(fixed, 0px 0px 0px 0px) +position(fixed, 0px 0px 0px 0px)
background: rgba(0, 0, 0, 0.2) background: rgba(0, 0, 0, 0.2)
text-align: left text-align: left
z-index: 99999999999 z-index: 10000
.ng-modal-bg .ng-modal-bg
+position(absolute, 0px 0px 0px 0px) +position(absolute, 0px 0px 0px 0px)

@ -27,6 +27,8 @@ See doc/COPYRIGHT.rdoc for more details.
++*/ ++*/
/* FIXME adapt modifications to the new action-menu component */
#context-menu li.folder ul { left:auto; right:168px; } #context-menu li.folder ul { left:auto; right:168px; }
#context-menu li.folder>ul { left:auto; right:148px; } #context-menu li.folder>ul { left:auto; right:148px; }
#context-menu li a.submenu { background:url("../images/bullet_arrow_left.png") left no-repeat; } #context-menu li a.submenu { background:url("../images/bullet_arrow_left.png") left no-repeat; }

@ -65,11 +65,11 @@
@mixin icon6-rules @mixin icon6-rules
padding: 0 7px 0 9px padding: 0 7px 0 9px
font-size: 12px font-size: 12px
@mixin icon-dropdown-rules @mixin icon-dropdown-rules
padding: 0 0px 0 3px padding: 0 0px 0 3px
font-size: 13px font-size: 13px
@mixin icon-button-rules @mixin icon-button-rules
padding: 0 5px 0 0px padding: 0 5px 0 0px
font-size: 13px font-size: 13px
@ -77,7 +77,7 @@
@mixin icon-context-rules @mixin icon-context-rules
padding: 0 4px 0 0 padding: 0 4px 0 0
color: $content_icon_color color: $content_icon_color
@mixin icon-table-rules @mixin icon-table-rules
padding: 0 0 0 0 padding: 0 0 0 0
@ -105,17 +105,17 @@
@include icon-common @include icon-common
content: attr(data-icon5) content: attr(data-icon5)
@include icon5-rules @include icon5-rules
[data-icon-dropwdown]:before [data-icon-dropwdown]:before
@include icon-common @include icon-common
content: attr(data-icon-dropdown) content: attr(data-icon-dropdown)
@include icon-dropdown-rules @include icon-dropdown-rules
[data-icon-button]:before [data-icon-button]:before
@include icon-common @include icon-common
content: attr(data-icon-button) content: attr(data-icon-button)
@include icon-button-rules @include icon-button-rules
[data-icon-table]:before [data-icon-table]:before
@include icon-common @include icon-common
content: attr(data-icon-table) content: attr(data-icon-table)
@ -147,15 +147,15 @@
// used for toggler icons in the project menu // used for toggler icons in the project menu
.icon6:before .icon6:before
@include icon6-rules @include icon6-rules
// used for arrow icons in buttons with dropdown // used for arrow icons in buttons with dropdown
.icon-dropdown:before .icon-dropdown:before
@include icon-dropdown-rules @include icon-dropdown-rules
// used for icons in buttons // used for icons in buttons
.icon-buttons:before .icon-buttons:before
@include icon-button-rules @include icon-button-rules
// used for icons in workpackage table // used for icons in workpackage table
.icon-table:before .icon-table:before
@include icon-table-rules @include icon-table-rules
@ -166,7 +166,7 @@
float: left float: left
// used for icons in the content area, which appear in context (menus) // used for icons in the content area, which appear in context (menus)
#context-menu .icon:before, .action-menu .icon:before,
.icon-context:before .icon-context:before
@include icon-context-rules @include icon-context-rules

@ -163,3 +163,4 @@ $content_box_border: 1px solid #EAEAEA !default
$content_box_bg_color: #FFFFFF !default $content_box_bg_color: #FFFFFF !default
$my_page_edit_box_border_color: #06799F !default $my_page_edit_box_border_color: #06799F !default
$action_menu_bg_color: #FFFFFF

@ -115,3 +115,11 @@ See doc/COPYRIGHT.rdoc for more details.
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
} }
.select2-drop {
z-index: 30001;
}
.select2-drop-mask {
z-index: 30000;
}

@ -76,7 +76,12 @@ class StatusesController < ApplicationController
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy def destroy
Status.find(params[:id]).destroy status = Status.find(params[:id])
if status.is_default?
flash[:error] = l(:error_unable_delete_default_status)
else
status.destroy
end
redirect_to :action => 'index' redirect_to :action => 'index'
rescue rescue
flash[:error] = l(:error_unable_delete_status) flash[:error] = l(:error_unable_delete_status)

@ -42,9 +42,11 @@ function recreateSortables() {
function updateSelect() { function updateSelect() {
s = $('block-select') s = $('block-select')
for (var i = 0; i < s.options.length; i++) { for (var i = 0; i < s.options.length; i++) {
if ($('block_' + s.options[i].value)) { var block = $('block_' + s.options[i].value);
if (block && block.visible()) {
s.options[i].disabled = true; s.options[i].disabled = true;
} else { } else {
if (block) { block.remove(); }
s.options[i].disabled = false; s.options[i].disabled = false;
} }
} }
@ -57,8 +59,9 @@ function afterAddBlock() {
} }
function removeBlock(block) { function removeBlock(block) {
Effect.DropOut(block); Effect.DropOut(block,{ 'afterFinish':function(){
updateSelect(); updateSelect();
}});
} }
//]]> //]]>
</script> </script>

@ -11,13 +11,14 @@
"angular": "~1.2.14", "angular": "~1.2.14",
"angular-animate": "~1.2.14", "angular-animate": "~1.2.14",
"angular-ui-select2": "latest", "angular-ui-select2": "latest",
"angular-ui-select2-sortable": "latest",
"angular-ui-date": "latest", "angular-ui-date": "latest",
"angular-i18n": "~1.3.0", "angular-i18n": "~1.3.0",
"angular-modal": "~0.3.0", "angular-modal": "~0.3.0",
"angular-sanitize": "~1.2.14", "angular-sanitize": "~1.2.14",
"jquery-migrate": "~1.2.1", "jquery-migrate": "~1.2.1",
"momentjs": "~2.6.0", "momentjs": "~2.6.0",
"ng-context-menu": "finnlabs/ng-context-menu#customized" "ng-context-menu": "finnlabs/ng-context-menu#context-sharing-with-multiple-targets"
}, },
"devDependencies": { "devDependencies": {
"mocha": "~1.14.0", "mocha": "~1.14.0",

@ -535,6 +535,7 @@ de:
error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}" error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}"
error_scm_not_found: "Eintrag und/oder Revision existiert nicht im Projektarchiv." error_scm_not_found: "Eintrag und/oder Revision existiert nicht im Projektarchiv."
error_unable_delete_status: "Der Ticket-Status konnte nicht gelöscht werden." error_unable_delete_status: "Der Ticket-Status konnte nicht gelöscht werden."
error_unable_delete_default_status: "Der Arbeitspaket-Status konnte nicht gelöscht werden, da dieser als Standard-Status markiert ist. Wählen Sie bitte zunächst einen anderen Arbeitspaket-Status als Standard-Status."
error_unable_to_connect: "Fehler beim Verbinden (%{value})" error_unable_to_connect: "Fehler beim Verbinden (%{value})"
error_workflow_copy_source: "Bitte wählen Sie einen Quell-Typ und eine Quell-Rolle." error_workflow_copy_source: "Bitte wählen Sie einen Quell-Typ und eine Quell-Rolle."
error_workflow_copy_target: "Bitte wählen Sie die Ziel-Typ und -Rollen." error_workflow_copy_target: "Bitte wählen Sie die Ziel-Typ und -Rollen."

@ -532,6 +532,7 @@ en:
error_scm_command_failed: "An error occurred when trying to access the repository: %{value}" error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
error_scm_not_found: "The entry or revision was not found in the repository." error_scm_not_found: "The entry or revision was not found in the repository."
error_unable_delete_status: "Unable to delete work package status" error_unable_delete_status: "Unable to delete work package status"
error_unable_delete_default_status: "Unable to delete default work package status. Please select another default work package status before deleting the current one."
error_unable_to_connect: "Unable to connect (%{value})" error_unable_to_connect: "Unable to connect (%{value})"
error_workflow_copy_source: "Please select a source type or role" error_workflow_copy_source: "Please select a source type or role"
error_workflow_copy_target: "Please select target type(s) and role(s)" error_workflow_copy_target: "Please select target type(s) and role(s)"

@ -69,6 +69,7 @@ de:
label_expanded: "aufgeklappt" label_expanded: "aufgeklappt"
label_expand_all: "Alle aufklappen" label_expand_all: "Alle aufklappen"
label_greater_or_equal: ">=" label_greater_or_equal: ">="
label_hide_column: "Spalte ausblenden"
label_in: "an" label_in: "an"
label_in_less_than: "in weniger als" label_in_less_than: "in weniger als"
label_in_more_than: "in mehr als" label_in_more_than: "in mehr als"
@ -78,10 +79,13 @@ de:
label_menu_collapse: "ausblenden" label_menu_collapse: "ausblenden"
label_menu_expand: "einblenden" label_menu_expand: "einblenden"
label_more_than_ago: "vor mehr als" label_more_than_ago: "vor mehr als"
label_move_column_left: "Spalte nach links verschieben"
label_move_column_right: "Spalte nach rechts verschieben"
label_no_data: "Nichts anzuzeigen" label_no_data: "Nichts anzuzeigen"
label_none: "kein" label_none: "kein"
label_not_contains: "enthält nicht" label_not_contains: "enthält nicht"
label_not_equals: "ist nicht" label_not_equals: "ist nicht"
label_open_menu: "Menü öffnen"
label_open_work_packages: "offen" label_open_work_packages: "offen"
label_remove_columns: "Ausgewählte Spalten entfernen" label_remove_columns: "Ausgewählte Spalten entfernen"
label_sort_by: "Sortiere nach" label_sort_by: "Sortiere nach"

@ -64,12 +64,12 @@ en:
label_collapsed: "collapsed" label_collapsed: "collapsed"
label_collapse_all: "Collapse all" label_collapse_all: "Collapse all"
label_contains: "contains" label_contains: "contains"
label_not_contains: "doesn't contain"
label_equals: "is" label_equals: "is"
label_expand: "Expand" label_expand: "Expand"
label_expanded: "expanded" label_expanded: "expanded"
label_expand_all: "Expand all" label_expand_all: "Expand all"
label_greater_or_equal: ">=" label_greater_or_equal: ">="
label_hide_column: "Hide column"
label_in: "in" label_in: "in"
label_in_less_than: "in less than" label_in_less_than: "in less than"
label_in_more_than: "in more than" label_in_more_than: "in more than"
@ -79,9 +79,13 @@ en:
label_menu_collapse: "collapse" label_menu_collapse: "collapse"
label_menu_expand: "expand" label_menu_expand: "expand"
label_more_than_ago: "more than days ago" label_more_than_ago: "more than days ago"
label_move_column_left: "Move column left"
label_move_column_right: "Move column right"
label_no_data: "No data to display" label_no_data: "No data to display"
label_none: "none" label_none: "none"
label_not_contains: "doesn't contain"
label_not_equals: "is not" label_not_equals: "is not"
label_open_menu: "Open menu"
label_open_work_packages: "open" label_open_work_packages: "open"
label_remove_columns: "Remove selected columns" label_remove_columns: "Remove selected columns"
label_sort_by: "Sort by" label_sort_by: "Sort by"

@ -70,8 +70,13 @@ See doc/COPYRIGHT.rdoc for more details.
* `#7149` Fix: Wrong success message when login is already in use * `#7149` Fix: Wrong success message when login is already in use
* `#7177` Fix: Journal not created in connection with deleted note * `#7177` Fix: Journal not created in connection with deleted note
* `#7384` Headlines in wiki table of content are broken * `#7384` Headlines in wiki table of content are broken
* `#7499` No default work package status possible
* `#7504` Timeline "Today" line not displayed properly
* `#7531` Projects menu not visible if there are no projects
* `#7533` Fixes: Deleted block not addable
* `#7562` Regression: creating ticket via API fails with HTTP 422 * `#7562` Regression: creating ticket via API fails with HTTP 422
* `#7608` Make highlight buttons styleable * `#7608` Make highlight buttons styleable
* `#7609` Fix: Blue icons in Telekom theme
* Allowed sending of mails with only cc: or bcc: fields * Allowed sending of mails with only cc: or bcc: fields
* Allow adding attachments to created work packages via planning elements controller * Allow adding attachments to created work packages via planning elements controller
* Remove unused rmagick dependency * Remove unused rmagick dependency

@ -46,7 +46,7 @@ Feature: Project creation with support for project type
Scenario: The admin may create a project with a project type Scenario: The admin may create a project with a project type
Given I am already admin Given I am already admin
When I go to the admin page When I go to the admin page
And I follow "Projects" And I follow the first link matching "Projects"
And I follow "New project" And I follow "New project"
Then I fill in "Fancy Pants" for "Name" Then I fill in "Fancy Pants" for "Name"
And I fill in "fancy-pants" for "Identifier" And I fill in "fancy-pants" for "Identifier"

@ -100,3 +100,14 @@ Feature: User session
When I login as blocked_user with password iamblocked When I login as blocked_user with password iamblocked
Then there should be a flash error message Then there should be a flash error message
And the flash message should contain "Invalid user or password" And the flash message should contain "Invalid user or password"
@javascript
Scenario: A deleted block is always visible in My page block list
Given I am already admin
When I go to the My page personalization page
And I select "Calendar" from the available widgets drop down
And I click on "Add"
Then the "Calendar" widget should be in the top block
And "Calendar" should be disabled in the my page available widgets drop down
When I click the first delete block link
Then "Calendar" should not be disabled in the my page available widgets drop down

@ -43,10 +43,19 @@ Then(/^I should see the widget "([^"]*)"$/) do |arg|
page.find("#widget_#{arg}").should_not be_nil page.find("#widget_#{arg}").should_not be_nil
end end
Then /^"(.+)" should be disabled in the my page available widgets drop down$/ do |widget_name| Then /^"(.+)" should( not)? be disabled in the my page available widgets drop down$/ do |widget_name , neg|
option_name = MyController.available_blocks.detect{|k, v| I18n.t(v) == widget_name}.first.dasherize option_name = MyController.available_blocks.detect{|k, v| I18n.t(v) == widget_name}.first.dasherize
unless neg
steps %Q{Then the "block-select" drop-down should have the following options disabled: steps %Q{Then the "block-select" drop-down should have the following options disabled:
| #{option_name} |} | #{option_name} |}
else
steps %Q{Then the "block-select" drop-down should have the following options enabled:
| #{option_name} |}
end
end end
When(/^I click the first delete block link$/) do
all(:xpath, "//a[@title='Remove widget']")[0].click
end

@ -444,6 +444,10 @@ When /^(?:|I )click on the first button matching "([^"]*)"$/ do |button|
first(:button, button).click first(:button, button).click
end end
When /^(?:|I )follow the first link matching "([^"]*)"$/ do |link|
first(:link, link).click
end
def find_lowest_containing_element text, selector def find_lowest_containing_element text, selector
elements = [] elements = []

@ -78,7 +78,7 @@ Feature: Updating work packages
| pe1 | | pe1 |
| pe2 | | pe2 |
And I hover over ".fixed_version .context_item" And I hover over ".fixed_version .context_item"
And I follow "none" within "#context-menu" And I follow "none" within "#work-package-context-menu"
Then I should see "Successful update" Then I should see "Successful update"
And I follow "pe1" And I follow "pe1"
And I should see "deleted (version1)" And I should see "deleted (version1)"
@ -89,7 +89,7 @@ Feature: Updating work packages
And I open the context menu on the work packages: And I open the context menu on the work packages:
| pe1 | | pe1 |
| pe2 | | pe2 |
And I follow "Edit" within "#context-menu" And I follow "Edit" within "#work-package-context-menu"
And I press "Submit" And I press "Submit"
Then I should see "Work Packages" within ".title-container" Then I should see "Work Packages" within ".title-container"
@ -100,6 +100,6 @@ Feature: Updating work packages
| pe1 | | pe1 |
| pe2 | | pe2 |
And I hover over ".assigned_to .context_item" And I hover over ".assigned_to .context_item"
And I follow "none" within "#context-menu" And I follow "none" within "#work-package-context-menu"
Then I should see "Successful update" Then I should see "Successful update"
Then the attribute "assigned_to" of work package "pe1" should be "" Then the attribute "assigned_to" of work package "pe1" should be ""

@ -128,7 +128,7 @@ Feature: Copying a work package
And I open the context menu on the work packages: And I open the context menu on the work packages:
| issue1 | | issue1 |
| issue2 | | issue2 |
And I follow "Copy" within "#context-menu" And I follow "Copy" within "#work-pacakge-context-menu"
Then I should see "Copy" within "#content" Then I should see "Copy" within "#content"
And I should not see "Move" within "#content" And I should not see "Move" within "#content"

@ -24,6 +24,7 @@ module.exports = function(config) {
"vendor/assets/components/angular/angular.js", "vendor/assets/components/angular/angular.js",
"vendor/assets/components/angular-mocks/angular-mocks.js", "vendor/assets/components/angular-mocks/angular-mocks.js",
"vendor/assets/components/angular-ui-select2/src/select2.js", "vendor/assets/components/angular-ui-select2/src/select2.js",
"vendor/assets/components/angular-ui-select2/src/select2sortable.js",
"vendor/assets/components/angular-modal/modal.js", "vendor/assets/components/angular-modal/modal.js",
"vendor/assets/components/angular-sanitize/angular-sanitize.js", "vendor/assets/components/angular-sanitize/angular-sanitize.js",
"vendor/assets/components/momentjs/moment.js", "vendor/assets/components/momentjs/moment.js",
@ -44,6 +45,7 @@ module.exports = function(config) {
'app/assets/javascripts/angular/helpers/timeline-table-helper.js', 'app/assets/javascripts/angular/helpers/timeline-table-helper.js',
'app/assets/javascripts/angular/helpers/function-decorators.js', 'app/assets/javascripts/angular/helpers/function-decorators.js',
'app/assets/javascripts/angular/helpers/url-params-helper.js', 'app/assets/javascripts/angular/helpers/url-params-helper.js',
'app/assets/javascripts/angular/helpers/queries-helper.js',
'app/assets/javascripts/angular/filters/work-packages-filters.js', 'app/assets/javascripts/angular/filters/work-packages-filters.js',

@ -83,6 +83,12 @@ describe('WorkPackagesController', function() {
}, 10); }, 10);
}, },
getAvailableUnusedColumns: function() {
return $timeout(function () {
return columnData;
}, 10);
},
getTotalEntries: function() { getTotalEntries: function() {
}, },

@ -0,0 +1,137 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('columnContextMenu Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates', 'openproject.models'));
beforeEach(inject(function($rootScope, $compile, _ContextMenuService_) {
var html;
html = '<column-context-menu></column-context-menu>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
ContextMenuService = _ContextMenuService_;
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
beforeEach(function() {
compile();
});
it('should render a surrounding div', function() {
expect(element.prop('tagName')).to.equal('DIV');
});
});
describe('when the context menu handler of a column is clicked', function() {
var I18n, QueryService;
var column = { name: 'status', title: 'Status' },
anotherColumn = { name: 'subject', title: 'Subject' },
columns = [column, anotherColumn],
query = Factory.build('Query', { columns: columns });
var directiveScope;
beforeEach(inject(function(_QueryService_) {
QueryService = _QueryService_;
sinon.stub(QueryService, 'getQuery').returns(query);
}));
afterEach(inject(function() {
QueryService.getQuery.restore();
}));
beforeEach(function() {
compile();
ContextMenuService.setContext({ column: column, columns: columns });
ContextMenuService.open('columnContextMenu');
scope.$apply();
directiveScope = element.children().scope();
});
it('fetches the column from the context handle context', function() {
expect(directiveScope.column).to.have.property('name').and.contain(column.name);
});
describe('and the group by option is clicked', function() {
beforeEach(function() {
directiveScope.groupBy(column.name);
});
it('changes the query group by', function() {
expect(query.groupBy).to.equal(column.name);
});
});
describe('and "move column right" is clicked', function() {
beforeEach(function() {
directiveScope.moveRight(column.name);
});
it('moves the column right', function() {
expect(columns[1]).to.equal(column);
});
});
describe('and "Sort ascending" is clicked', function() {
var Sortation;
beforeEach(inject(function(_Sortation_) {
Sortation = _Sortation_;
query.sortation = new Sortation();
directiveScope.sortAscending(column.name);
}));
it('updates the query sortation', function() {
expect(query.sortation.getPrimarySortationCriterion()).to.deep.equal({ field: column.name, direction: 'asc' });
});
});
describe('and "Hide column" is clicked', function() {
beforeEach(function() {
directiveScope.hideColumn(column.name);
});
it('removes the column from the query columns', function() {
expect(query.columns).to.not.include(column);
});
});
});
});

@ -32,7 +32,7 @@ describe('workPackageContextMenu Directive', function() {
var compile, element, rootScope, scope; var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives')); beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates')); beforeEach(module('templates', 'openproject.models'));
beforeEach(inject(function($rootScope, $compile, _ContextMenuService_) { beforeEach(inject(function($rootScope, $compile, _ContextMenuService_) {
var html; var html;
@ -74,9 +74,12 @@ describe('workPackageContextMenu Directive', function() {
var directListElements; var directListElements;
beforeEach(function() { beforeEach(function() {
ContextMenuService.setContext({rows: [], row: {object: workPackage}});
compile(); compile();
ContextMenuService.setContext({rows: [], row: {object: workPackage}});
ContextMenuService.open('workPackageContextMenu');
scope.$apply();
directListElements = element.find('.menu > li:not(.folder)'); directListElements = element.find('.menu > li:not(.folder)');
}); });

@ -31,7 +31,8 @@
describe('WorkPackageContextMenuHelper', function() { describe('WorkPackageContextMenuHelper', function() {
var WorkPackageContextMenuHelper; var WorkPackageContextMenuHelper;
beforeEach(module('openproject.workPackages.helpers')); beforeEach(module('openproject.workPackages.helpers', 'openproject.models'));
beforeEach(inject(function(_WorkPackageContextMenuHelper_) { beforeEach(inject(function(_WorkPackageContextMenuHelper_) {
WorkPackageContextMenuHelper = _WorkPackageContextMenuHelper_; WorkPackageContextMenuHelper = _WorkPackageContextMenuHelper_;
})); }));

@ -78,4 +78,21 @@ describe('WorkPackagesTableHelper', function() {
}); });
}); });
describe('mapColumnNamesToColumns', function() {
// What do we even need this helper for at the moment?
var columns = [{ name: 'cheese' },
{ name: 'biscuits' },
{ name: 'grapes' },
{ name: 'wine' },
{ name: 'pandas' }];
var identifiers = ['cheese', 'wine'];
it('should get the columns for the names given', function() {
var selectedColumns = WorkPackagesTableHelper.mapIdentifiersToColumns(columns, identifiers);
expect(selectedColumns).to.deep.equal([{ name: 'cheese' }, { name: 'wine' }]);
});
});
}); });

@ -48,7 +48,7 @@ module Redmine::MenuManager::TopMenuHelper
def render_projects_top_menu_node def render_projects_top_menu_node
return "" if User.current.anonymous? and Setting.login_required? return "" if User.current.anonymous? and Setting.login_required?
return "" if User.current.number_of_known_projects.zero? return "" if User.current.anonymous? and User.current.number_of_known_projects.zero?
heading = link_to l(:label_project_plural), heading = link_to l(:label_project_plural),
{ :controller => '/projects', { :controller => '/projects',

@ -8,7 +8,7 @@
<div class="tl-toolbar-container" style="width: 1px; height: 20px; background-color: rgb(0, 0, 0);"></div> <div class="tl-toolbar-container" style="width: 1px; height: 20px; background-color: rgb(0, 0, 0);"></div>
<div class="tl-toolbar-container"> <div class="tl-toolbar-container">
<a href="javascript://" title="Zoom out" class="icon tl-icon-zoomout" ng-click="decreaseZoom()"></a> <a href="javascript://" title="Zoom out" class="icon icon-zoom-out" ng-click="decreaseZoom()"></a>
</div> </div>
<div class="tl-toolbar-container" style="width: 100px; height: 20px;"> <div class="tl-toolbar-container" style="width: 100px; height: 20px;">
@ -16,7 +16,7 @@
</div> </div>
<div class="tl-toolbar-container"> <div class="tl-toolbar-container">
<a href="javascript://" title="Zoom in" class="icon tl-icon-zoomin" ng-click="increaseZoom()"></a> <a href="javascript://" title="Zoom in" class="icon icon-zoom-in" ng-click="increaseZoom()"></a>
</div> </div>
@ -31,7 +31,7 @@
<div class="tl-toolbar-container" style="width: 1px; height: 20px; background-color: rgb(0, 0, 0);"></div> <div class="tl-toolbar-container" style="width: 1px; height: 20px; background-color: rgb(0, 0, 0);"></div>
<div class="tl-toolbar-container"> <div class="tl-toolbar-container">
<a href="javascript://" title="Reset Outline" class="icon tl-icon-outline" ng-click="resetOutline()"></a> <a href="javascript://" title="Reset Outline" class="icon icon-outline" ng-click="resetOutline()"></a>
</div> </div>
<div class="tl-toolbar-container"> <div class="tl-toolbar-container">

@ -24,6 +24,7 @@
</modal-loading> </modal-loading>
<work-package-context-menu></work-package-context-menu> <work-package-context-menu></work-package-context-menu>
<column-context-menu></column-context-menu>
</div> </div>

@ -0,0 +1,28 @@
<div id="column-context-menu" class="action-menu" ng-show="opened">
<ul class="menu">
<li ng-click="groupBy(column.name)">
<a href="#"><span ng-bind="I18n.t('js.work_packages.query.group_by')"/> {{column.title}}</a>
</li>
<li ng-click="sortAscending(column.name)">
<a href="#"><span ng-bind="I18n.t('js.label_sort_by')"/> <span ng-bind="column.title"/> <span ng-bind="I18n.t('js.label_ascending')"/></a>
</li>
<li ng-click="sortDescending(column.name)">
<a href="#"><span ng-bind="I18n.t('js.label_sort_by')"/> <span ng-bind="column.title"/> <span ng-bind="I18n.t('js.label_descending')"/></a>
</li>
<li ng-click="moveLeft(column.name)">
<a href="#"><span ng-bind="I18n.t('js.label_move_column_left')"/></a>
</li>
<li ng-click="moveRight(column.name)">
<a href="#"><span ng-bind="I18n.t('js.label_move_column_right')"/></a>
</li>
<li ng-click="hideColumn(column.name)">
<a href="#"><span ng-bind="I18n.t('js.label_hide_column')"/></a>
</li>
</ul>
</div>

@ -4,5 +4,21 @@
<h1>Columns</h1> <h1>Columns</h1>
<div>
<label for="selected_columns">Selected Columns</label>
<input id="selected_columns_new"
type="hidden"
ui-select2-sortable="select2Options"
simple-query="getObjectsData"
ng-model="selectedColumnsData"
multiple
sortable></input>
</div>
<div>
<button ng-click="updateSelectedColumns()">Apply</button>
<button ng-click="modal.closeMe()">Cancel</button>
</div>
</div> </div>
</div> </div>

@ -8,7 +8,7 @@
multiple="multiple" multiple="multiple"
name="available_columns[]" name="available_columns[]"
ng-model="markedAvailableColumns" ng-model="markedAvailableColumns"
ng-options="column.name as column.title for column in availableColumns" ng-options="column.name as column.title for column in availableUnusedColumns"
size="10" size="10"
style="width:150px"> style="width:150px">
</select> </select>
@ -18,13 +18,13 @@
<input id="add_selected_columns" <input id="add_selected_columns"
type="button" type="button"
value="→" value="→"
ng-click="moveColumns(markedAvailableColumns, availableColumns, columns, true)" ng-click="showColumns(markedAvailableColumns)"
title="{{ I18n.t('js.label_add_columns') }}" title="{{ I18n.t('js.label_add_columns') }}"
ng-disabled="loading"><br> ng-disabled="loading"><br>
<label class="hidden-for-sighted" for="add_selected_columns">{{ I18n.t('js.label_remove_columns') }}</label> <label class="hidden-for-sighted" for="add_selected_columns">{{ I18n.t('js.label_remove_columns') }}</label>
<input type="button" <input type="button"
value="←" value="←"
ng-click="moveColumns(markedSelectedColumns, columns, availableColumns)" ng-click="hideColumns(markedSelectedColumns)"
title="{{ I18n.t('js.label_remove_columns') }}" title="{{ I18n.t('js.label_remove_columns') }}"
ng-disabled="loading"> ng-disabled="loading">
</td> </td>

@ -6,4 +6,5 @@
lang-attribute lang-attribute
lang="{{locale}}">{{headerTitle}}</a> lang="{{locale}}">{{headerTitle}}</a>
<span ng-if="!sortable">{{headerTitle}}</span> <span ng-if="!sortable">{{headerTitle}}</span>
<span ng-transclude/>
</span> </span>

@ -1,4 +1,4 @@
<div id="context-menu" ng-show="opened"> <div id="work-package-context-menu" class="action-menu" ng-show="opened">
<ul class="menu"> <ul class="menu">
<li ng-repeat="(action, link) in permittedActions" <li ng-repeat="(action, link) in permittedActions"
ng-click="triggerContextMenuAction(action, link)" ng-click="triggerContextMenuAction(action, link)"

@ -24,7 +24,14 @@
header-name="column.name" header-name="column.name"
header-title="column.title" header-title="column.title"
sortable="column.sortable" sortable="column.sortable"
query="query"/> query="query">
<span has-context-menu
target="columnContextMenu"
process-event="adaptVerticalPosition"
trigger-on-event="click">
<icon-wrapper icon-name="pulldown-arrow1" title="{{I18n.t('js.label_open_menu')}}"></icon-wrapper>
</span>
</th>
</tr> </tr>
</thead> </thead>
@ -83,6 +90,7 @@
<tr work-package-row <tr work-package-row
id="work-package-{{ row.object.id }}" id="work-package-{{ row.object.id }}"
has-context-menu has-context-menu
target="workPackageContextMenu"
process-event="adaptVerticalPosition" process-event="adaptVerticalPosition"
ng-class="[ ng-class="[
'hascontextmenu', 'hascontextmenu',

@ -175,6 +175,23 @@ describe StatusesController do
it_behaves_like :redirect it_behaves_like :redirect
end end
context "default" do
let!(:status_default) { FactoryGirl.create(:status,
is_default: true) }
before do
delete :destroy, id: status_default.id
end
it_behaves_like :statuses
it_behaves_like :redirect
it "shows the right flash message" do
expect(flash[:error]).to eq(I18n.t('error_unable_delete_default_status'))
end
end
end end
describe :update_work_package_done_ratio do describe :update_work_package_done_ratio do

@ -84,7 +84,7 @@ describe 'Work package index accessibility' do
it do it do
expect(sort_header).not_to be_nil expect(sort_header).not_to be_nil
expect(sort_header.find("span")[:title]).to eq(sort_text) expect(sort_header.find("span.sort-header")[:title]).to eq(sort_text)
end end
end end

@ -0,0 +1,40 @@
require 'spec_helper'
describe "layouts/base" do
include Redmine::MenuManager::MenuHelper
helper Redmine::MenuManager::MenuHelper
let!(:user) { FactoryGirl.create :user }
let!(:anonymous) { FactoryGirl.create(:anonymous) }
before do
view.stub(:current_menu_item).and_return("overview")
view.stub(:default_breadcrumb)
controller.stub(:default_search_scope)
end
describe "projects menu visibility" do
context "when the user is not logged in" do
before do
User.stub(:current).and_return anonymous
view.stub(:current_user).and_return anonymous
render
end
it "the projects menu should not be displayed" do
expect(response).to_not have_text("Projects")
end
end
context "when the user is logged in" do
before do
User.stub(:current).and_return user
view.stub(:current_user).and_return user
render
end
it "the projects menu should be displayed" do
expect(response).to have_text("Projects")
end
end
end
end
Loading…
Cancel
Save