diff --git a/app/assets/javascripts/angular/controllers/dialogs/columns.js b/app/assets/javascripts/angular/controllers/dialogs/columns.js index 8b75750b11..ed35566a8a 100644 --- a/app/assets/javascripts/angular/controllers/dialogs/columns.js +++ b/app/assets/javascripts/angular/controllers/dialogs/columns.js @@ -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.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(); + } }]); diff --git a/app/assets/javascripts/angular/controllers/work-packages-controller.js b/app/assets/javascripts/angular/controllers/work-packages-controller.js index 08967e3cb0..8d47fde316 100644 --- a/app/assets/javascripts/angular/controllers/work-packages-controller.js +++ b/app/assets/javascripts/angular/controllers/work-packages-controller.js @@ -116,6 +116,7 @@ angular.module('openproject.workPackages.controllers') WorkPackagesTableService.setColumns($scope.query.columns); WorkPackagesTableService.addColumnMetaData(meta); WorkPackagesTableService.setRows(WorkPackagesTableHelper.getRows(workPackages, $scope.query.groupBy)); + WorkPackagesTableService.setGroupBy($scope.query.groupBy); WorkPackagesTableService.setBulkLinks(bulkLinks); // query data @@ -139,10 +140,9 @@ angular.module('openproject.workPackages.controllers') } function initAvailableColumns() { - return QueryService.getAvailableColumns($scope.projectIdentifier) + return QueryService.getAvailableUnusedColumns($scope.projectIdentifier) .then(function(data){ - $scope.availableColumns = WorkPackagesTableHelper.getColumnDifference(data.available_columns, $scope.columns); - return $scope.availableColumns; + $scope.availableUnusedColumns = data; }); } diff --git a/app/assets/javascripts/angular/directives/work_packages/column-context-menu-directive.js b/app/assets/javascripts/angular/directives/work_packages/column-context-menu-directive.js new file mode 100644 index 0000000000..92df11eca2 --- /dev/null +++ b/app/assets/javascripts/angular/directives/work_packages/column-context-menu-directive.js @@ -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)); + }; + } + }; +}]); diff --git a/app/assets/javascripts/angular/directives/work_packages/options-dropdown-directive.js b/app/assets/javascripts/angular/directives/work_packages/options-dropdown-directive.js index 151c08ab82..43abc4370d 100644 --- a/app/assets/javascripts/angular/directives/work_packages/options-dropdown-directive.js +++ b/app/assets/javascripts/angular/directives/work_packages/options-dropdown-directive.js @@ -61,9 +61,15 @@ angular.module('openproject.workPackages.directives') } }; + scope.showColumnsModal = function(){ + scope.$emit('hideAllDropdowns'); + columnsModal.activate(); + }; + scope.toggleDisplaySums = function(){ + scope.$emit('hideAllDropdowns'); scope.query.displaySums = !scope.query.displaySums; - } + }; } }; }]); diff --git a/app/assets/javascripts/angular/directives/work_packages/query-columns-directive.js b/app/assets/javascripts/angular/directives/work_packages/query-columns-directive.js index 1474771708..01ff088745 100644 --- a/app/assets/javascripts/angular/directives/work_packages/query-columns-directive.js +++ b/app/assets/javascripts/angular/directives/work_packages/query-columns-directive.js @@ -28,7 +28,11 @@ angular.module('openproject.workPackages.directives') -.directive('queryColumns', ['WorkPackagesTableHelper', 'WorkPackageService', function(WorkPackagesTableHelper, WorkPackageService) { +.directive('queryColumns', [ + 'WorkPackagesTableHelper', + 'WorkPackageService', + 'QueryService', + function(WorkPackagesTableHelper, WorkPackageService, QueryService) { return { restrict: 'E', @@ -37,14 +41,14 @@ angular.module('openproject.workPackages.directives') compile: function(tElement) { return { pre: function(scope) { - scope.moveColumns = function (columnNames, fromColumns, toColumns, requires_extension) { - angular.forEach(columnNames, function(columnName){ - removeColumn(columnName, fromColumns, function(removedColumn){ - toColumns.push(removedColumn); - }); - }); + scope.showColumns = function(columnNames) { + QueryService.showColumns(columnNames); + + extendRowsWithColumnData(columnNames); // TODO move to QueryService + }; - if (requires_extension) extendRowsWithColumnData(columnNames); + scope.hideColumns = function(columnNames) { + QueryService.hideColumns(columnNames); }; scope.moveSelectedColumnBy = function(by) { @@ -52,6 +56,7 @@ angular.module('openproject.workPackages.directives') WorkPackagesTableHelper.moveColumnBy(scope.columns, nameOfColumnToBeMoved, by); }; + // TODO move to WorkPackagesService function extendRowsWithColumnData(columnNames) { var workPackages = scope.rows.map(function(row) { return row.object; @@ -64,11 +69,6 @@ angular.module('openproject.workPackages.directives') scope.withLoading(WorkPackageService.augmentWorkPackagesWithColumnsData, params) .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; - } } }; } diff --git a/app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js b/app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js index 699cd56eb3..7fd3dc696d 100644 --- a/app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js +++ b/app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js @@ -33,6 +33,7 @@ angular.module('openproject.workPackages.directives') return { restrict: 'A', templateUrl: '/templates/work_packages/sort_header.html', + transclude: true, scope: { query: '=', headerName: '=', @@ -41,29 +42,35 @@ angular.module('openproject.workPackages.directives') locale: '=' }, link: function(scope, element, attributes) { - scope.$watch('query.sortation.sortElements', function(newValue, oldValue){ - if (scope.headerName != newValue[0].field) scope.currentSortDirection = null; + scope.$watch('query.sortation.sortElements', function(sortElements){ + var latestSortElement = sortElements[0]; + + if (scope.headerName !== latestSortElement.field) { + scope.currentSortDirection = null; + } else { + scope.currentSortDirection = latestSortElement.direction; + } + + setFullTitle(); }, true); scope.performSort = function(){ - targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName); + var targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName); + scope.query.setSortation(targetSortation); scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName); - scope.setFullTitle(); }; - scope.setFullTitle = function(){ + function setFullTitle() { 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'); scope.fullTitle = sortDirectionText + " " + I18n.t('js.label_sorted_by') + ' \"' + scope.headerTitle + '\"'; } else { scope.fullTitle = (I18n.t('js.label_sort_by') + ' \"' + scope.headerTitle + '\"'); } - }; - - scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName); - scope.setFullTitle(); + } } }; }]); diff --git a/app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js b/app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js index 8f915f7ff1..6ca57936f5 100644 --- a/app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js +++ b/app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js @@ -15,20 +15,25 @@ angular.module('openproject.workPackages.directives') scope: {}, templateUrl: '/templates/work_packages/work_package_context_menu.html', link: function(scope, element, attrs) { + var contextMenuName = 'workPackageContextMenu'; + scope.I18n = I18n; - scope.opened = false; // wire up context menu event handler - - ContextMenuService.setTarget(element); + ContextMenuService.registerMenuElement(contextMenuName, element); scope.contextMenu = ContextMenuService.getContextMenu(); 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() { - updateContextMenu(getWorkPackagesFromContext(scope.contextMenu.context)); + scope.$watch('contextMenu.context.row', function(row) { + if (row && scope.contextMenu.targetMenu === contextMenuName) { + updateContextMenu(getWorkPackagesFromContext(scope.contextMenu.context)); + } }); scope.triggerContextMenuAction = function(action, link) { @@ -53,8 +58,6 @@ angular.module('openproject.workPackages.directives') }); 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) { // TODO wire up to API and processs API response diff --git a/app/assets/javascripts/angular/helpers/work-packages-table-helper.js b/app/assets/javascripts/angular/helpers/work-packages-table-helper.js index 4b74f49645..9892c1509a 100644 --- a/app/assets/javascripts/angular/helpers/work-packages-table-helper.js +++ b/app/assets/javascripts/angular/helpers/work-packages-table-helper.js @@ -89,16 +89,16 @@ angular.module('openproject.workPackages.helpers') }, getColumnDifference: function (allColumns, columns) { - var columnValues = columns.map(function(column){ + var identifiers = columns.map(function(column){ 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 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) { // TODO maybe extend the Array prototype var newPosition = index + positions; 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); } }, @@ -142,6 +148,20 @@ angular.module('openproject.workPackages.helpers') 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) { return rows .filter(function(row) { diff --git a/app/assets/javascripts/angular/models/query.js b/app/assets/javascripts/angular/models/query.js index df662fa024..7fe3be3d7d 100644 --- a/app/assets/javascripts/angular/models/query.js +++ b/app/assets/javascripts/angular/models/query.js @@ -162,6 +162,10 @@ angular.module('openproject.models') }); }, + getSelectedColumns: function(){ + return this.columns; + }, + getParamColumns: function(){ var selectedColumns = this.columns.map(function(column) { return column.name; diff --git a/app/assets/javascripts/angular/models/timelines/mixins/ui.js b/app/assets/javascripts/angular/models/timelines/mixins/ui.js index f7e91ae8a9..0afcac9b1b 100644 --- a/app/assets/javascripts/angular/models/timelines/mixins/ui.js +++ b/app/assets/javascripts/angular/models/timelines/mixins/ui.js @@ -819,7 +819,7 @@ angular.module('openproject.timelines.models') }) ).attr({ 'stroke': 'red', - 'stroke-dasharray': '- ' + 'stroke-dasharray': '3,3' }); var setDateTime = 5 * 60 * 1000; diff --git a/app/assets/javascripts/angular/openproject-app.js b/app/assets/javascripts/angular/openproject-app.js index 5fac66eb41..37887f7611 100644 --- a/app/assets/javascripts/angular/openproject-app.js +++ b/app/assets/javascripts/angular/openproject-app.js @@ -104,6 +104,7 @@ angular.module('openproject.timeEntries.controllers', []); // main app var openprojectApp = angular.module('openproject', [ 'ui.select2', + 'ui.select2.sortable', 'ui.date', 'ui.router', 'openproject.uiComponents', diff --git a/app/assets/javascripts/angular/services/query-service.js b/app/assets/javascripts/angular/services/query-service.js index 75dc33bffd..74644b994c 100644 --- a/app/assets/javascripts/angular/services/query-service.js +++ b/app/assets/javascripts/angular/services/query-service.js @@ -28,12 +28,32 @@ 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', - function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService, I18n) { +.service('QueryService', [ + '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 availableColumns = [], availableFilterValues = {}, availableFilters = {}; + var availableColumns = [], + availableUnusedColumns = [], + availableFilterValues = {}, + availableFilters = {}; var totalEntries; @@ -79,6 +99,14 @@ angular.module('openproject.services') return totalEntries; }, + hideColumns: function(columnNames) { + WorkPackagesTableHelper.moveColumns(columnNames, this.getSelectedColumns(), availableColumns); + }, + + showColumns: function(columnNames) { + WorkPackagesTableHelper.moveColumns(columnNames, availableColumns, this.getSelectedColumns()); + }, + // data loading getAvailableGroupedQueries: function(projectIdentifier) { @@ -87,10 +115,37 @@ angular.module('openproject.services') 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) { + // 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(); - 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){ @@ -193,6 +248,8 @@ angular.module('openproject.services') return availableFilters[projectIdentifier]; }, + // synchronization + saveQuery: function() { var url = PathHelper.apiProjectQueryPath(query.project_id, query.id); 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) { method = method || 'GET'; success = success || function(response){ diff --git a/app/assets/javascripts/angular/services/work-packages-table-service.js b/app/assets/javascripts/angular/services/work-packages-table-service.js index f5a7bfd5e9..74380ae51e 100644 --- a/app/assets/javascripts/angular/services/work-packages-table-service.js +++ b/app/assets/javascripts/angular/services/work-packages-table-service.js @@ -28,7 +28,10 @@ angular.module('openproject.workPackages.services') -.service('WorkPackagesTableService', ['$filter', function($filter) { +.service('WorkPackagesTableService', [ + '$filter', + 'QueryService', + function($filter, QueryService) { var workPackagesTableData = { allRowsChecked: false }; @@ -77,10 +80,25 @@ angular.module('openproject.workPackages.services') setRows: function(rows) { workPackagesTableData.rows = rows; }, + getRows: function() { 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) { var rows = workPackagesTableData.rows; var index = rows.indexOf(row); @@ -91,8 +109,14 @@ angular.module('openproject.workPackages.services') angular.forEach(rows, function(row) { WorkPackagesTableService.removeRow(row); }); - } + }, + sortBy: function(columnName, direction) { + QueryService.getQuery().sortation.addSortElement({ + field: columnName, + direction: direction + }); + } }; return WorkPackagesTableService; diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index eb90c47bb7..72a8dbd666 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -58,6 +58,7 @@ //= require angular-modal //= require angular-ui-router //= require angular-ui-select2 +//= require angular-ui-select2-sortable //= require angular-ui-date/src/date //= require angular-sanitize diff --git a/app/assets/stylesheets/content/_action_menu_main.md b/app/assets/stylesheets/content/_action_menu_main.md index 991018ce7a..6e7e392a0a 100644 --- a/app/assets/stylesheets/content/_action_menu_main.md +++ b/app/assets/stylesheets/content/_action_menu_main.md @@ -1 +1,51 @@ # Action menu + +``` +
+ + +``` diff --git a/app/assets/stylesheets/content/_action_menu_main.sass b/app/assets/stylesheets/content/_action_menu_main.sass index 1f4ffaf354..efc7784c11 100644 --- a/app/assets/stylesheets/content/_action_menu_main.sass +++ b/app/assets/stylesheets/content/_action_menu_main.sass @@ -82,3 +82,49 @@ ul.action_menu_more .message-reply-menu @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 diff --git a/app/assets/stylesheets/content/_context_menu.sass b/app/assets/stylesheets/content/_context_menu.sass index b65415c696..dd4aabf581 100644 --- a/app/assets/stylesheets/content/_context_menu.sass +++ b/app/assets/stylesheets/content/_context_menu.sass @@ -34,106 +34,9 @@ padding: 0 border: 0 -#context-menu - position: absolute - font-size: 0.9em - left: -7px - top: -7px - padding: 6px - z-index: 21 - ul - @include context_menu_defaults - width: 140px - list-style: none +#work-package-context-menu, #column-context-menu + &.action-menu 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 cursor: context-menu diff --git a/app/assets/stylesheets/content/_modal.sass b/app/assets/stylesheets/content/_modal.sass index 18202fd25e..d390ca643d 100644 --- a/app/assets/stylesheets/content/_modal.sass +++ b/app/assets/stylesheets/content/_modal.sass @@ -14,7 +14,7 @@ $ng-modal-image-width: $ng-modal-image-height +position(fixed, 0px 0px 0px 0px) background: rgba(0, 0, 0, 0.2) text-align: left - z-index: 99999999999 + z-index: 10000 .ng-modal-bg +position(absolute, 0px 0px 0px 0px) diff --git a/app/assets/stylesheets/context_menu_rtl.css b/app/assets/stylesheets/context_menu_rtl.css index 66c7b5888f..5258418270 100644 --- a/app/assets/stylesheets/context_menu_rtl.css +++ b/app/assets/stylesheets/context_menu_rtl.css @@ -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:148px; } #context-menu li a.submenu { background:url("../images/bullet_arrow_left.png") left no-repeat; } diff --git a/app/assets/stylesheets/fonts/_openproject_icon_font.sass b/app/assets/stylesheets/fonts/_openproject_icon_font.sass index 1ed09ae63b..d5d500a0ea 100644 --- a/app/assets/stylesheets/fonts/_openproject_icon_font.sass +++ b/app/assets/stylesheets/fonts/_openproject_icon_font.sass @@ -65,11 +65,11 @@ @mixin icon6-rules padding: 0 7px 0 9px font-size: 12px - + @mixin icon-dropdown-rules padding: 0 0px 0 3px font-size: 13px - + @mixin icon-button-rules padding: 0 5px 0 0px font-size: 13px @@ -77,7 +77,7 @@ @mixin icon-context-rules padding: 0 4px 0 0 color: $content_icon_color - + @mixin icon-table-rules padding: 0 0 0 0 @@ -105,17 +105,17 @@ @include icon-common content: attr(data-icon5) @include icon5-rules - + [data-icon-dropwdown]:before @include icon-common content: attr(data-icon-dropdown) @include icon-dropdown-rules - + [data-icon-button]:before @include icon-common content: attr(data-icon-button) @include icon-button-rules - + [data-icon-table]:before @include icon-common content: attr(data-icon-table) @@ -147,15 +147,15 @@ // used for toggler icons in the project menu .icon6:before @include icon6-rules - + // used for arrow icons in buttons with dropdown .icon-dropdown:before @include icon-dropdown-rules - + // used for icons in buttons .icon-buttons:before @include icon-button-rules - + // used for icons in workpackage table .icon-table:before @include icon-table-rules @@ -166,7 +166,7 @@ float: left // used for icons in the content area, which appear in context (menus) -#context-menu .icon:before, +.action-menu .icon:before, .icon-context:before @include icon-context-rules diff --git a/app/assets/stylesheets/global/_variables.sass b/app/assets/stylesheets/global/_variables.sass index a497618d1c..c84e911bd3 100644 --- a/app/assets/stylesheets/global/_variables.sass +++ b/app/assets/stylesheets/global/_variables.sass @@ -163,3 +163,4 @@ $content_box_border: 1px solid #EAEAEA !default $content_box_bg_color: #FFFFFF !default $my_page_edit_box_border_color: #06799F !default +$action_menu_bg_color: #FFFFFF diff --git a/app/assets/stylesheets/select2_customizing.css.erb b/app/assets/stylesheets/select2_customizing.css.erb index 695863fe9b..75a649b11d 100644 --- a/app/assets/stylesheets/select2_customizing.css.erb +++ b/app/assets/stylesheets/select2_customizing.css.erb @@ -115,3 +115,11 @@ See doc/COPYRIGHT.rdoc for more details. color: #fff; font-weight: bold; } + +.select2-drop { + z-index: 30001; +} + +.select2-drop-mask { + z-index: 30000; +} diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index f04bd7d9f4..84fbb0e0eb 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -76,7 +76,12 @@ class StatusesController < ApplicationController verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } 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' rescue flash[:error] = l(:error_unable_delete_status) diff --git a/app/views/my/page_layout.html.erb b/app/views/my/page_layout.html.erb index b218c44aa8..c74d111e9b 100644 --- a/app/views/my/page_layout.html.erb +++ b/app/views/my/page_layout.html.erb @@ -42,9 +42,11 @@ function recreateSortables() { function updateSelect() { s = $('block-select') 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; } else { + if (block) { block.remove(); } s.options[i].disabled = false; } } @@ -57,8 +59,9 @@ function afterAddBlock() { } function removeBlock(block) { - Effect.DropOut(block); - updateSelect(); + Effect.DropOut(block,{ 'afterFinish':function(){ + updateSelect(); + }}); } //]]> diff --git a/bower.json b/bower.json index 1af210674c..d7cb08a6d7 100644 --- a/bower.json +++ b/bower.json @@ -11,13 +11,14 @@ "angular": "~1.2.14", "angular-animate": "~1.2.14", "angular-ui-select2": "latest", + "angular-ui-select2-sortable": "latest", "angular-ui-date": "latest", "angular-i18n": "~1.3.0", "angular-modal": "~0.3.0", "angular-sanitize": "~1.2.14", "jquery-migrate": "~1.2.1", "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": { "mocha": "~1.14.0", diff --git a/config/locales/de.yml b/config/locales/de.yml index 32f9925524..9f307aca8e 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -535,6 +535,7 @@ de: 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_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_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." diff --git a/config/locales/en.yml b/config/locales/en.yml index 88a4a83631..187adfa6b4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -532,6 +532,7 @@ en: 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_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_workflow_copy_source: "Please select a source type or role" error_workflow_copy_target: "Please select target type(s) and role(s)" diff --git a/config/locales/js-de.yml b/config/locales/js-de.yml index f1e3fabdec..4ee183e2ec 100644 --- a/config/locales/js-de.yml +++ b/config/locales/js-de.yml @@ -69,6 +69,7 @@ de: label_expanded: "aufgeklappt" label_expand_all: "Alle aufklappen" label_greater_or_equal: ">=" + label_hide_column: "Spalte ausblenden" label_in: "an" label_in_less_than: "in weniger als" label_in_more_than: "in mehr als" @@ -78,10 +79,13 @@ de: label_menu_collapse: "ausblenden" label_menu_expand: "einblenden" 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_none: "kein" label_not_contains: "enthält nicht" label_not_equals: "ist nicht" + label_open_menu: "Menü öffnen" label_open_work_packages: "offen" label_remove_columns: "Ausgewählte Spalten entfernen" label_sort_by: "Sortiere nach" diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 44adfff4f3..a5d117f16c 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -64,12 +64,12 @@ en: label_collapsed: "collapsed" label_collapse_all: "Collapse all" label_contains: "contains" - label_not_contains: "doesn't contain" label_equals: "is" label_expand: "Expand" label_expanded: "expanded" label_expand_all: "Expand all" label_greater_or_equal: ">=" + label_hide_column: "Hide column" label_in: "in" label_in_less_than: "in less than" label_in_more_than: "in more than" @@ -79,9 +79,13 @@ en: label_menu_collapse: "collapse" label_menu_expand: "expand" 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_none: "none" + label_not_contains: "doesn't contain" label_not_equals: "is not" + label_open_menu: "Open menu" label_open_work_packages: "open" label_remove_columns: "Remove selected columns" label_sort_by: "Sort by" diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index ed96328fb9..d0f176f4dc 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -70,8 +70,13 @@ See doc/COPYRIGHT.rdoc for more details. * `#7149` Fix: Wrong success message when login is already in use * `#7177` Fix: Journal not created in connection with deleted note * `#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 * `#7608` Make highlight buttons styleable +* `#7609` Fix: Blue icons in Telekom theme * Allowed sending of mails with only cc: or bcc: fields * Allow adding attachments to created work packages via planning elements controller * Remove unused rmagick dependency diff --git a/features/project_types/project_creation_with_type.feature b/features/project_types/project_creation_with_type.feature index 44080ddc95..dac8488af3 100644 --- a/features/project_types/project_creation_with_type.feature +++ b/features/project_types/project_creation_with_type.feature @@ -46,7 +46,7 @@ Feature: Project creation with support for project type Scenario: The admin may create a project with a project type Given I am already admin When I go to the admin page - And I follow "Projects" + And I follow the first link matching "Projects" And I follow "New project" Then I fill in "Fancy Pants" for "Name" And I fill in "fancy-pants" for "Identifier" diff --git a/features/session/user_session.feature b/features/session/user_session.feature index 8f5cefcd47..d93d63f792 100644 --- a/features/session/user_session.feature +++ b/features/session/user_session.feature @@ -100,3 +100,14 @@ Feature: User session When I login as blocked_user with password iamblocked Then there should be a flash error message 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 diff --git a/features/step_definitions/my_page_steps.rb b/features/step_definitions/my_page_steps.rb index 1865be9583..c34784b87f 100644 --- a/features/step_definitions/my_page_steps.rb +++ b/features/step_definitions/my_page_steps.rb @@ -43,10 +43,19 @@ Then(/^I should see the widget "([^"]*)"$/) do |arg| page.find("#widget_#{arg}").should_not be_nil 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 + unless neg steps %Q{Then the "block-select" drop-down should have the following options disabled: | #{option_name} |} + else + steps %Q{Then the "block-select" drop-down should have the following options enabled: + | #{option_name} |} + end + end +When(/^I click the first delete block link$/) do + all(:xpath, "//a[@title='Remove widget']")[0].click +end diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb index 90cc409bd1..f86a1722f3 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -444,6 +444,10 @@ When /^(?:|I )click on the first button matching "([^"]*)"$/ do |button| first(:button, button).click end +When /^(?:|I )follow the first link matching "([^"]*)"$/ do |link| + first(:link, link).click +end + def find_lowest_containing_element text, selector elements = [] diff --git a/features/work_packages/bulk.feature b/features/work_packages/bulk.feature index 9e1670887e..a8e1eed92a 100644 --- a/features/work_packages/bulk.feature +++ b/features/work_packages/bulk.feature @@ -78,7 +78,7 @@ Feature: Updating work packages | pe1 | | pe2 | 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" And I follow "pe1" And I should see "deleted (version1)" @@ -89,7 +89,7 @@ Feature: Updating work packages And I open the context menu on the work packages: | pe1 | | pe2 | - And I follow "Edit" within "#context-menu" + And I follow "Edit" within "#work-package-context-menu" And I press "Submit" Then I should see "Work Packages" within ".title-container" @@ -100,6 +100,6 @@ Feature: Updating work packages | pe1 | | pe2 | 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 the attribute "assigned_to" of work package "pe1" should be "" diff --git a/features/work_packages/moves/work_package_moves_new_copy.feature b/features/work_packages/moves/work_package_moves_new_copy.feature index 003eabb0c2..cf92c9b23a 100644 --- a/features/work_packages/moves/work_package_moves_new_copy.feature +++ b/features/work_packages/moves/work_package_moves_new_copy.feature @@ -128,7 +128,7 @@ Feature: Copying a work package And I open the context menu on the work packages: | issue1 | | issue2 | - And I follow "Copy" within "#context-menu" + And I follow "Copy" within "#work-pacakge-context-menu" Then I should see "Copy" within "#content" And I should not see "Move" within "#content" diff --git a/karma.conf.js b/karma.conf.js index b62c09c120..b46315d193 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function(config) { "vendor/assets/components/angular/angular.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/select2sortable.js", "vendor/assets/components/angular-modal/modal.js", "vendor/assets/components/angular-sanitize/angular-sanitize.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/function-decorators.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', diff --git a/karma/tests/controllers/work-packages-controller-test.js b/karma/tests/controllers/work-packages-controller-test.js index 0b7bbe7286..e2e956ce2a 100644 --- a/karma/tests/controllers/work-packages-controller-test.js +++ b/karma/tests/controllers/work-packages-controller-test.js @@ -83,6 +83,12 @@ describe('WorkPackagesController', function() { }, 10); }, + getAvailableUnusedColumns: function() { + return $timeout(function () { + return columnData; + }, 10); + }, + getTotalEntries: function() { }, diff --git a/karma/tests/directives/work_packages/column-context-menu-directive-test.js b/karma/tests/directives/work_packages/column-context-menu-directive-test.js new file mode 100644 index 0000000000..e616235ea8 --- /dev/null +++ b/karma/tests/directives/work_packages/column-context-menu-directive-test.js @@ -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 = '