Merge pull request #1431 from opf/refactoring/action-menus

Refactoring/action menus
pull/1436/head
Alex Coles 11 years ago
commit 4b563fba97
  1. 72
      app/assets/javascripts/angular/directives/work_packages/column-context-menu-directive.js
  2. 33
      app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js
  3. 106
      app/assets/javascripts/angular/directives/work_packages/work-package-context-menu-directive.js
  4. 6
      app/assets/javascripts/angular/openproject-app.js
  5. 131
      app/assets/javascripts/angular/ui_components/has-dropdown-menu-directive.js
  6. 85
      app/assets/javascripts/angular/work_packages/column-context-menu.js
  7. 117
      app/assets/javascripts/angular/work_packages/work-package-context-menu.js
  8. 2
      app/assets/javascripts/application.js.erb
  9. 1
      app/assets/stylesheets/content/_context_menu.sass
  10. 3
      app/assets/stylesheets/layout/_drop_down.sass
  11. 2
      bower.json
  12. 1
      config/locales/de.yml
  13. 1
      config/locales/en.yml
  14. 1
      config/locales/js-de.yml
  15. 1
      config/locales/js-en.yml
  16. 6
      karma.conf.js
  17. 32
      karma/tests/directives/work_packages/sort-header-directive-test.js
  18. 92
      karma/tests/work_packages/column-context-menu-test.js
  19. 81
      karma/tests/work_packages/work-package-context-menu-test.js
  20. 3
      public/templates/work_packages.list.html
  21. 34
      public/templates/work_packages/column_context_menu.html
  22. 3
      public/templates/work_packages/sort_header.html
  23. 2
      public/templates/work_packages/work_package_context_menu.html
  24. 16
      public/templates/work_packages/work_packages_table.html
  25. 106
      spec/features/accessibility/work_packages/work_package_query_spec.rb

@ -1,72 +0,0 @@
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.isGroupable = WorkPackagesTableService.isGroupable(scope.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) {
ContextMenuService.close();
QueryService.hideColumns(new Array(columnName));
};
}
};
}]);

@ -28,12 +28,13 @@
angular.module('openproject.workPackages.directives')
.directive('sortHeader', ['I18n', function(I18n){
.directive('sortHeader', [
'I18n',
function(I18n){
return {
restrict: 'A',
templateUrl: '/templates/work_packages/sort_header.html',
transclude: true,
scope: {
query: '=',
headerName: '=',
@ -41,7 +42,8 @@ angular.module('openproject.workPackages.directives')
sortable: '=',
locale: '='
},
link: function(scope, element, attributes) {
require: 'hasDropdownMenu',
link: function(scope, element, attributes, dropdownMenuCtrl) {
scope.$watch('query.sortation.sortElements', function(sortElements){
var latestSortElement = sortElements[0];
@ -54,17 +56,6 @@ angular.module('openproject.workPackages.directives')
setFullTitle();
}, true);
scope.$watch('currentSortDirection', function(sort) {
element.toggleClass('active-column', !!sort);
});
scope.performSort = function(){
var targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName);
scope.query.setSortation(targetSortation);
scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName);
};
function setFullTitle() {
if(!scope.sortable) scope.fullTitle = '';
@ -72,9 +63,21 @@ angular.module('openproject.workPackages.directives')
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.fullTitle = I18n.t('js.label_open_menu');
}
}
// active-column class setting
function setActiveColumnClass() {
element.toggleClass('active-column', !!scope.currentSortDirection || scope.dropDownMenuOpened);
}
scope.$watch(dropdownMenuCtrl.opened, function(opened) {
scope.dropDownMenuOpened = opened;
setActiveColumnClass();
});
scope.$watch('currentSortDirection', setActiveColumnClass);
}
};
}]);

@ -1,106 +0,0 @@
angular.module('openproject.workPackages.directives')
.directive('workPackageContextMenu', [
'ContextMenuService',
'WorkPackagesTableHelper',
'WorkPackageContextMenuHelper',
'WorkPackageService',
'WorkPackagesTableService',
'I18n',
'$window',
function(ContextMenuService, WorkPackagesTableHelper, WorkPackageContextMenuHelper, WorkPackageService, WorkPackagesTableService, I18n, $window) {
return {
restrict: 'EA',
replace: true,
scope: {},
templateUrl: '/templates/work_packages/work_package_context_menu.html',
link: function(scope, element, attrs) {
var contextMenuName = 'workPackageContextMenu';
scope.I18n = I18n;
scope.hideResourceActions = true;
// wire up context menu event handler
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;
});
scope.$watch('contextMenu.context.row', function(row) {
if (row && scope.contextMenu.targetMenu === contextMenuName) {
updateContextMenu(getWorkPackagesFromContext(scope.contextMenu.context));
}
});
scope.triggerContextMenuAction = function(action, link) {
if (action === 'delete') {
deleteSelectedWorkPackages();
} else {
$window.location.href = link;
}
};
function deleteSelectedWorkPackages() {
if (!deleteConfirmed()) return;
var rows = WorkPackagesTableHelper.getSelectedRows(scope.contextMenu.context.rows);
WorkPackageService.performBulkDelete(getWorkPackagesFromContext(scope.contextMenu.context))
.success(function(data, status) {
// TODO wire up to API and processs API response
scope.$emit('flashMessage', {
isError: false,
text: I18n.t('js.work_packages.message_successful_bulk_delete')
});
WorkPackagesTableService.removeRows(rows);
})
.error(function(data, status) {
// TODO wire up to API and processs API response
scope.$emit('flashMessage', {
isError: true,
text: I18n.t('js.work_packages.message_error_during_bulk_delete')
});
});
}
function deleteConfirmed() {
return $window.confirm(I18n.t('js.text_work_packages_destroy_confirmation'));
}
function updateContextMenu(workPackages) {
scope.permittedActions = WorkPackageContextMenuHelper.getPermittedActions(workPackages);
}
function getWorkPackagesFromSelectedRows(rows) {
var selectedRows = WorkPackagesTableHelper.getSelectedRows(rows);
return WorkPackagesTableHelper.getWorkPackagesFromRows(selectedRows);
}
function getWorkPackagesFromContext(context) {
if (!context.row) return [];
context.row.checked = true;
var workPackagefromContext = context.row.object;
var workPackagesfromSelectedRows = getWorkPackagesFromSelectedRows(context.rows);
if (workPackagesfromSelectedRows.length === 0) {
return [workPackagefromContext];
} else if (workPackagesfromSelectedRows.indexOf(workPackagefromContext) === -1) {
return [workPackagefromContext].concat(workPackagesfromSelectedRows);
} else {
return workPackagesfromSelectedRows;
}
}
}
};
}]);

@ -67,7 +67,8 @@ angular.module('openproject.workPackages', [
'openproject.workPackages.controllers',
'openproject.workPackages.filters',
'openproject.workPackages.directives',
'openproject.uiComponents'
'openproject.uiComponents',
'ng-context-menu'
]);
angular.module('openproject.workPackages.services', []);
angular.module('openproject.workPackages.helpers', [
@ -88,8 +89,7 @@ angular.module('openproject.workPackages.controllers', [
angular.module('openproject.workPackages.directives', [
'openproject.uiComponents',
'openproject.services',
'openproject.workPackages.services',
'ng-context-menu'
'openproject.workPackages.services'
]);
// messages

@ -0,0 +1,131 @@
angular.module('openproject.uiComponents')
.directive('hasDropdownMenu', [
'$injector',
'$window',
'$parse',
function($injector, $window, $parse) {
function getCssPositionProperties(dropdown, trigger) {
var hOffset = 0,
vOffset = 0;
// Styling logic taken from jQuery-dropdown plugin: https://github.com/plapier/jquery-dropdown
// (dual MIT/GPL-Licensed)
// Position the dropdown relative-to-parent or relative-to-document
if (dropdown.hasClass('dropdown-relative')) {
return {
left: dropdown.hasClass('dropdown-anchor-right') ?
trigger.position().left - (dropdown.outerWidth(true) - trigger.outerWidth(true)) - parseInt(trigger.css('margin-right')) + hOffset :
trigger.position().left + parseInt(trigger.css('margin-left')) + hOffset,
top: trigger.position().top + trigger.outerHeight(true) - parseInt(trigger.css('margin-top')) + vOffset
};
} else {
return {
left: dropdown.hasClass('dropdown-anchor-right') ?
trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth()) + hOffset : trigger.offset().left + hOffset,
top: trigger.offset().top + trigger.outerHeight() + vOffset
};
}
}
return {
restrict: 'A',
controller: [function() {
var dropDownMenuOpened = false;
this.open = function() {
dropDownMenuOpened = true;
};
this.close = function() {
dropDownMenuOpened = false;
};
this.opened = function() {
return dropDownMenuOpened;
};
}],
link: function(scope, element, attrs, ctrl) {
var contextMenu = $injector.get(attrs.target),
locals = {},
win = angular.element($window),
menuElement,
triggerOnEvent = attrs.triggerOnEvent || 'click';
/* contextMenu is a mandatory attribute and used to bind a specific context
menu to the trigger event
triggerOnEvent allows for binding the event for opening the menu to "click" */
// prepare locals, these define properties to be passed on to the context menu scope
var localKeys = attrs.locals.split(',').map(function(local) {
return local.trim();
});
angular.forEach(localKeys, function(key) {
locals[key] = scope[key];
});
function toggle() {
active() ? close() : open();
}
function active() {
return contextMenu.active() && ctrl.opened();
}
function open() {
ctrl.open();
contextMenu.open(locals)
.then(function(menuElement) {
menuElement.css(getCssPositionProperties(menuElement, element));
});
}
function close() {
ctrl.close();
contextMenu.close();
}
element.bind(triggerOnEvent, function(event) {
event.preventDefault();
event.stopPropagation();
scope.$apply(function() {
toggle();
});
scope.$root.$broadcast('openproject.markDropdownsAsClosed', element);
});
scope.$on('openproject.markDropdownsAsClosed', function(event, target) {
if (element !== target && ctrl.opened()) {
scope.$apply(ctrl.close);
}
});
win.bind('keyup', function(event) {
if (contextMenu.active() && event.keyCode === 27) {
scope.$apply(function() {
close();
});
}
});
function handleWindowClickEvent(event) {
if (contextMenu.active() && event.button !== 2) {
scope.$apply(function() {
close();
});
}
}
// Firefox treats a right-click as a click and a contextmenu event while other browsers
// just treat it as a contextmenu event
win.bind('click', handleWindowClickEvent);
win.bind(triggerOnEvent, handleWindowClickEvent);
}
};
}]);

@ -0,0 +1,85 @@
//-- 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.
//++
angular.module('openproject.workPackages')
.factory('ColumnContextMenu', [
'ngContextMenu',
function(ngContextMenu) {
return ngContextMenu({
controller: 'ColumnContextMenuController',
controllerAs: 'contextMenu',
templateUrl: '/templates/work_packages/column_context_menu.html'
});
}])
.controller('ColumnContextMenuController', [
'$scope',
'ColumnContextMenu',
'I18n',
'QueryService',
'WorkPackagesTableHelper',
'WorkPackagesTableService',
'columnsModal',
function($scope, ColumnContextMenu, I18n, QueryService, WorkPackagesTableHelper, WorkPackagesTableService, columnsModal) {
$scope.I18n = I18n;
$scope.isGroupable = WorkPackagesTableService.isGroupable($scope.column);
// context menu actions
$scope.groupBy = function(columnName) {
QueryService.getQuery().groupBy = columnName;
};
$scope.sortAscending = function(columnName) {
WorkPackagesTableService.sortBy(columnName || 'id', 'asc');
};
$scope.sortDescending = function(columnName) {
WorkPackagesTableService.sortBy(columnName || 'id', '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) {
ColumnContextMenu.close();
QueryService.hideColumns(new Array(columnName));
};
$scope.insertColumns = function() {
columnsModal.activate();
};
}]);

@ -0,0 +1,117 @@
//-- 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.
//++
angular.module('openproject.workPackages')
.factory('WorkPackageContextMenu', [
'ngContextMenu',
function(ngContextMenu) {
return ngContextMenu({
controller: 'WorkPackageContextMenuController',
controllerAs: 'contextMenu',
templateUrl: '/templates/work_packages/work_package_context_menu.html'
});
}])
.controller('WorkPackageContextMenuController', [
'$scope',
'WorkPackagesTableHelper',
'WorkPackageContextMenuHelper',
'WorkPackageService',
'WorkPackagesTableService',
'I18n',
'$window',
function($scope, WorkPackagesTableHelper, WorkPackageContextMenuHelper, WorkPackageService, WorkPackagesTableService, I18n, $window) {
$scope.I18n = I18n;
$scope.hideResourceActions = true;
$scope.$watch('row', function() {
$scope.row.checked = true;
$scope.permittedActions = WorkPackageContextMenuHelper.getPermittedActions(getSelectedWorkPackages());
});
$scope.triggerContextMenuAction = function(action, link) {
if (action === 'delete') {
deleteSelectedWorkPackages();
} else {
$window.location.href = link;
}
};
function deleteSelectedWorkPackages() {
if (!deleteConfirmed()) return;
var rows = WorkPackagesTableHelper.getSelectedRows($scope.rows);
WorkPackageService.performBulkDelete(getSelectedWorkPackages())
.success(function(data, status) {
// TODO wire up to API and processs API response
$scope.$emit('flashMessage', {
isError: false,
text: I18n.t('js.work_packages.message_successful_bulk_delete')
});
WorkPackagesTableService.removeRows(rows);
})
.error(function(data, status) {
// TODO wire up to API and processs API response
$scope.$emit('flashMessage', {
isError: true,
text: I18n.t('js.work_packages.message_error_during_bulk_delete')
});
});
}
function deleteConfirmed() {
return $window.confirm(I18n.t('js.text_work_packages_destroy_confirmation'));
}
function getWorkPackagesFromSelectedRows() {
var selectedRows = WorkPackagesTableHelper.getSelectedRows($scope.rows);
return WorkPackagesTableHelper.getWorkPackagesFromRows(selectedRows);
}
function getSelectedWorkPackages() {
var workPackagefromContext = $scope.row.object;
var workPackagesfromSelectedRows = getWorkPackagesFromSelectedRows();
if (workPackagesfromSelectedRows.length === 0) {
return [workPackagefromContext];
} else if (workPackagesfromSelectedRows.indexOf(workPackagefromContext) === -1) {
return [workPackagefromContext].concat(workPackagesfromSelectedRows);
} else {
return workPackagesfromSelectedRows;
}
}
}]);

@ -66,7 +66,7 @@
//= require angular-truncate
//= require angular-feature-flags/dist/featureFlags.js
//= require ng-context-menu
//= require angular-context-menu
//= require openproject-ui_components/app/assets/javascripts/angular/ui-components-app
//= require ./angular/openproject-app

@ -37,6 +37,7 @@
#work-package-context-menu, #column-context-menu
&.action-menu
position: absolute
z-index: 1000
.hascontextmenu
cursor: context-menu

@ -36,6 +36,9 @@
#settingsDropdown, #tasksDropdown
margin: 10px 0 0 0
#column-context-menu
margin: 55px 0 0 0
.dropdown
position: absolute
z-index: 9999999

@ -22,7 +22,7 @@
"jquery-migrate": "~1.2.1",
"momentjs": "~2.6.0",
"moment-timezone": "~0.0.6",
"ng-context-menu": "finnlabs/ng-context-menu#context-sharing-with-multiple-targets"
"angular-context-menu": "0.1.1"
},
"devDependencies": {
"mocha": "~1.14.0",

@ -801,6 +801,7 @@ de:
label_not_contains: "enthält nicht"
label_not_equals: "ist nicht"
label_notify_member_plural: "Aktualisierungen per E-Mail verschicken"
label_open_menu: "Menü öffnen"
label_open_work_packages: "offen"
label_open_work_packages_plural: "offen"
label_optional_description: "Beschreibung"

@ -798,6 +798,7 @@ en:
label_not_contains: "doesn't contain"
label_not_equals: "is not"
label_notify_member_plural: "Email updates"
label_open_menu: "Open menu"
label_open_work_packages: "open"
label_open_work_packages_plural: "open"
label_optional_description: "Description"

@ -217,6 +217,7 @@ de:
move_column_left: "Spalte nach links"
move_column_right: "Spalte nach rechts"
hide_column: "Spalte verbergen"
insert_columns: "Spalten hinzufügen ..."
filters: "Filter"
display_sums: "Summen anzeigen"
toolbar:

@ -220,6 +220,7 @@ en:
move_column_left: "Move column left"
move_column_right: "Move column right"
hide_column: "Hide column"
insert_columns: "Insert columns ..."
filters: "Filters"
display_sums: "Display Sums"
toolbar:

@ -29,8 +29,8 @@ module.exports = function(config) {
"vendor/assets/components/angular-truncate/src/truncate.js",
"vendor/assets/components/angular-sanitize/angular-sanitize.js",
"vendor/assets/components/momentjs/moment.js",
"vendor/assets/components/angular-context-menu/dist/angular-context-menu.js",
'vendor/assets/components/select2/select2.js',
"vendor/assets/components/ng-context-menu/dist/ng-context-menu.js",
"vendor/assets/components/openproject-ui_components/app/assets/javascripts/angular/ui-components-app.js",
"app/assets/javascripts/angular/openproject-app.js",
@ -102,6 +102,8 @@ module.exports = function(config) {
"app/assets/javascripts/angular/controllers/dialogs/share.js",
"app/assets/javascripts/angular/controllers/dialogs/sorting.js",
"app/assets/javascripts/angular/work_packages/**/*.js",
'app/assets/javascripts/autocompleter.js',
'app/assets/javascripts/members_select_boxes.js',
'app/assets/javascripts/openproject.js',
@ -113,7 +115,7 @@ module.exports = function(config) {
'karma/lib/rosie.js',
'karma/tests/test-helper.js',
'karma/factories/*factory.js',
'vendor/assets/components/jquery-mockjax/jquery.mockjax.js',
'karma/tests/asset_functions.js',

@ -43,9 +43,19 @@ describe('sortHeader Directive', function() {
rootScope = $rootScope;
scope = $rootScope.$new();
// Mock hasDropdownManu controller
var dropdownMenuController = function() {
this.open = function() {
return true;
};
};
compile = function() {
$compile(element1)(scope);
$compile(element2)(scope);
angular.forEach([element1, element2], function(element){
element.data('$hasDropdownMenuController', dropdownMenuController);
$compile(element)(scope);
});
scope.$digest();
};
}));
@ -57,8 +67,10 @@ describe('sortHeader Directive', function() {
}));
describe('rendering multiple headers', function(){
var query;
beforeEach(function(){
var query = new Query({
query = new Query({
});
query.setSortation(new Sortation([]));
scope.query = query;
@ -93,10 +105,12 @@ describe('sortHeader Directive', function() {
var link1 = element1.find('span a').first();
expect(link1.hasClass('sort asc')).to.not.be.ok;
link1.click();
query.sortation.addSortElement({ field: scope.headerName1, direction: 'asc' });
scope.$digest();
expect(link1.hasClass('sort asc')).to.be.ok;
link1.click();
query.sortation.addSortElement({ field: scope.headerName1, direction: 'desc' });
scope.$digest();
expect(link1.hasClass('sort desc')).to.be.ok;
});
@ -108,11 +122,15 @@ describe('sortHeader Directive', function() {
scope.$apply();
var link1 = element1.find('span a').first();
link1.click();
query.sortation.addSortElement({ field: scope.headerName1, direction: 'asc' });
scope.$digest();
expect(link1.hasClass('sort asc')).to.be.ok;
var link2 = element2.find('span a').first();
link2.click();
query.sortation.addSortElement({ field: scope.headerName2, direction: 'asc' });
scope.$digest();
expect(link2.hasClass('sort asc')).to.be.ok;
expect(link1.hasClass('sort asc')).to.not.be.ok;
});

@ -28,37 +28,36 @@
/*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>';
describe('columnContextMenu', function() {
var container, contextMenu, $rootScope, scope;
beforeEach(module('ng-context-menu',
'openproject.workPackages',
'openproject.workPackages.controllers',
'openproject.models',
'templates'));
beforeEach(function() {
var html = '<div></div>';
container = angular.element(html);
});
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
ContextMenuService = _ContextMenuService_;
beforeEach(inject(function(_$rootScope_, _ngContextMenu_, $templateCache) {
$rootScope = _$rootScope_;
ngContextMenu = _ngContextMenu_;
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
var template = $templateCache.get('/templates/work_packages/column_context_menu.html');
$templateCache.put('column_context_menu.html', [200, template, {}]);
describe('element', function() {
beforeEach(function() {
compile();
contextMenu = ngContextMenu({
controller: 'ColumnContextMenuController',
controllerAs: 'contextMenu',
container: container,
templateUrl: 'column_context_menu.html'
});
it('should render a surrounding div', function() {
expect(element.prop('tagName')).to.equal('DIV');
});
});
contextMenu.open({x: 0, y: 0});
}));
describe('when the context menu handler of a column is clicked', function() {
var I18n, QueryService;
@ -77,22 +76,20 @@ describe('columnContextMenu Directive', function() {
}));
beforeEach(function() {
compile();
$rootScope.column = column;
$rootScope.columns = columns;
$rootScope.$digest();
ContextMenuService.setContext({ column: column, columns: columns });
ContextMenuService.open('columnContextMenu');
scope.$apply();
directiveScope = element.children().scope();
scope = container.children().scope();
});
it('fetches the column from the context handle context', function() {
expect(directiveScope.column).to.have.property('name').and.contain(column.name);
expect($rootScope.column).to.have.property('name').and.contain(column.name);
});
describe('and the group by option is clicked', function() {
beforeEach(function() {
directiveScope.groupBy(column.name);
scope.groupBy(column.name);
});
it('changes the query group by', function() {
@ -102,7 +99,7 @@ describe('columnContextMenu Directive', function() {
describe('and "move column right" is clicked', function() {
beforeEach(function() {
directiveScope.moveRight(column.name);
scope.moveRight(column.name);
});
it('moves the column right', function() {
@ -116,7 +113,7 @@ describe('columnContextMenu Directive', function() {
beforeEach(inject(function(_Sortation_) {
Sortation = _Sortation_;
query.sortation = new Sortation();
directiveScope.sortAscending(column.name);
scope.sortAscending(column.name);
}));
it('updates the query sortation', function() {
@ -126,12 +123,33 @@ describe('columnContextMenu Directive', function() {
describe('and "Hide column" is clicked', function() {
beforeEach(function() {
directiveScope.hideColumn(column.name);
scope.hideColumn(column.name);
});
it('removes the column from the query columns', function() {
expect(query.columns).to.not.include(column);
});
});
describe('and "Insert columns" is clicked', function() {
var activateFn, columnsModal;
beforeEach(inject(function(_columnsModal_) {
columnsModal = _columnsModal_;
activateFn = sinon.stub(columnsModal, 'activate');
}));
afterEach(inject(function() {
columnsModal.activate.restore();
}));
beforeEach(function() {
scope.insertColumns();
});
it('opens the columns dialog', function() {
expect(activateFn).to.have.been.called;
});
});
});
});

@ -1,3 +1,4 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
@ -28,37 +29,35 @@
/*jshint expr: true*/
describe('workPackageContextMenu Directive', function() {
var compile, element, rootScope, scope;
describe('workPackageContextMenu', function() {
var container, contextMenu, $rootScope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates', 'openproject.models'));
beforeEach(module('ng-context-menu',
'openproject.workPackages',
'openproject.models',
'templates'));
beforeEach(inject(function($rootScope, $compile, _ContextMenuService_) {
var html;
html = '<work-package-context-menu></work-package-context-menu>';
beforeEach(function() {
var html = '<div></div>';
container = angular.element(html);
});
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
ContextMenuService = _ContextMenuService_;
beforeEach(inject(function(_$rootScope_, _ngContextMenu_, $templateCache) {
$rootScope = _$rootScope_;
ngContextMenu = _ngContextMenu_;
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
var template = $templateCache.get('/templates/work_packages/work_package_context_menu.html');
$templateCache.put('work_package_context_menu.html', [200, template, {}]);
describe('element', function() {
beforeEach(function() {
compile();
contextMenu = ngContextMenu({
controller: 'WorkPackageContextMenuController',
controllerAs: 'contextMenu',
container: container,
templateUrl: 'work_package_context_menu.html'
});
it('should render a surrounding div', function() {
expect(element.prop('tagName')).to.equal('DIV');
});
});
contextMenu.open({x: 0, y: 0});
}));
describe('when the context menu context contains one work package', function() {
var I18n;
@ -73,16 +72,6 @@ describe('workPackageContextMenu Directive', function() {
});
var directListElements;
beforeEach(function() {
compile();
ContextMenuService.setContext({rows: [], row: {object: workPackage}});
ContextMenuService.open('workPackageContextMenu');
scope.$apply();
directListElements = element.find('.menu > li:not(.folder)');
});
beforeEach(inject(function(_I18n_) {
I18n = _I18n_;
sinon.stub(I18n, 't').withArgs('js.button_' + actions[0]).returns('anything');
@ -91,6 +80,15 @@ describe('workPackageContextMenu Directive', function() {
I18n.t.restore();
}));
beforeEach(function() {
$rootScope.rows = [];
$rootScope.row = {object: workPackage};
$rootScope.$digest();
directListElements = container.find('.menu > li:not(.folder)');
});
it('lists link tags for any permitted action', function(){
expect(directListElements.length).to.equal(2);
});
@ -99,17 +97,17 @@ describe('workPackageContextMenu Directive', function() {
expect(directListElements[0].className).to.equal(actions[0]);
});
it('adds an icon from the icon fonts to each list element', function() {
expect(element.find('.' + actions[0] +' a').attr('class')).to.include('icon-' + actions[0]);
it('adds an icon from the icon fonts to each list container', function() {
expect(container.find('.' + actions[0] +' a').attr('class')).to.include('icon-' + actions[0]);
});
xit('translates the action name', function() {
expect(element.find('.' + actions[0] +' a').contents()).to.equal('anything');
expect(container.find('.' + actions[0] +' a').contents()).to.equal('anything');
// TODO find out how to stub I18n.t inside directive
});
it('sets the checked property of the row within the context to true', function() {
expect(ContextMenuService.getContextMenu().context.row.checked).to.be.true;
expect($rootScope.row.checked).to.be.true;
});
describe('when delete is permitted on a work package', function() {
@ -123,10 +121,11 @@ describe('workPackageContextMenu Directive', function() {
});
beforeEach(function() {
ContextMenuService.setContext({rows: [], row: {object: workPackage}});
compile();
$rootScope.rows = [];
$rootScope.row = {object: workPackage};
$rootScope.$digest();
directListElements = element.find('.menu > li:not(.folder)');
directListElements = container.find('.menu > li:not(.folder)');
});
it('displays a link triggering deleteWorkPackages within the scope', function() {

@ -23,9 +23,6 @@
<modal-loading>
</modal-loading>
<work-package-context-menu></work-package-context-menu>
<column-context-menu></column-context-menu>
</div>
<div class="work-packages--details" ui-view
feature-flag="detailsView"></div>

@ -1,13 +1,5 @@
<div id="column-context-menu" class="action-menu" ng-show="opened">
<ul>
<li ng-click="groupBy(column.name)"
ng-if="isGroupable">
<a href="#">
<i class="icon-action-menu icon-group-by2"></i>
<span ng-bind="I18n.t('js.work_packages.query.group')"/>
</a>
</li>
<div id="column-context-menu" class="action-menu dropdown-relative dropdown-anchor-right">
<ul class="menu">
<li ng-click="sortAscending(column.name)">
<a href="#">
<i class="icon-action-menu icon-sort-ascending"></i>
@ -22,8 +14,16 @@
</a>
</li>
<li ng-click="groupBy(column.name)"
ng-if="isGroupable">
<i class="icon-action-menu icon-group-by2"></i>
<a href="#">
<span ng-bind="I18n.t('js.work_packages.query.group')"/>
</a>
</li>
<li ng-click="moveLeft(column.name)"
ng-if="columns.indexOf(column) !== 0">
ng-if="column && columns.indexOf(column) !== 0">
<i class="icon-action-menu icon-column-left"></i>
<a href="#">
<span ng-bind="I18n.t('js.work_packages.query.move_column_left')"/>
@ -31,19 +31,27 @@
</li>
<li ng-click="moveRight(column.name)"
ng-if="columns.indexOf(column) !== columns.length - 1">
ng-if="column && columns.indexOf(column) !== columns.length - 1">
<i class="icon-action-menu icon-column-right"></i>
<a href="#">
<span ng-bind="I18n.t('js.work_packages.query.move_column_right')"/>
</a>
</li>
<li ng-click="hideColumn(column.name)">
<li ng-if="column"
ng-click="hideColumn(column.name)">
<i class="icon-action-menu icon-delete2"></i>
<a href="#">
<span ng-bind="I18n.t('js.work_packages.query.hide_column')"/>
</a>
</li>
<li ng-click="insertColumns()">
<i class="icon-action-menu icon-columns"></i>
<a href="#">
<span ng-bind="I18n.t('js.work_packages.query.insert_columns')"/>
</a>
</li>
</ul>
</div>

@ -2,9 +2,8 @@
<a href="javascript://"
ng-if="sortable"
ng-class="[currentSortDirection && 'sort', currentSortDirection]"
ng-click="performSort()"
lang-attribute
lang="{{locale}}">{{headerTitle}}</a>
<span ng-if="!sortable">{{headerTitle}}</span>
<span ng-transclude/>
<icon-wrapper icon-name="pulldown-arrow1" title="{{I18n.t('js.label_open_menu')}}"></icon-wrapper>
</span>

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

@ -15,22 +15,22 @@
<th sort-header
header-name="'id'"
header-title="'#'"
has-dropdown-menu
target="ColumnContextMenu"
locals="columns, column"
sortable="true"
query="query"
update-results="updateResults()"/>
<th sort-header ng-repeat="column in columns"
has-dropdown-menu
target="ColumnContextMenu"
locals="columns, column"
locale="column.custom_field && columns.custom_field.name_locale || I18n.locale"
header-name="column.name"
header-title="column.title"
sortable="column.sortable"
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>
</thead>
@ -94,8 +94,8 @@
<tr work-package-row
id="work-package-{{ row.object.id }}"
has-context-menu
target="workPackageContextMenu"
process-event="adaptVerticalPosition"
target="WorkPackageContextMenu"
locals="rows, row"
ng-class="[
'issue',
'hascontextmenu',

@ -35,6 +35,8 @@ describe 'Work package index accessibility' do
let!(:work_package) { FactoryGirl.create(:work_package,
project: project) }
let(:work_packages_page) { WorkPackagesPage.new(project) }
let(:sort_ascending_selector) { '.icon-sort-ascending' }
let(:sort_decending_selector) { '.icon-sort-descending' }
before do
allow(User).to receive(:current).and_return(user)
@ -73,131 +75,117 @@ describe 'Work package index accessibility' do
end
describe 'Sort link', js: true do
def sort_link
find(sort_link_selector)
def column_header_link
find(column_header_link_selector)
end
def sort_ascending_link
find(sort_ascending_selector)
end
def sort_decending_link
find(sort_decending_selector)
end
shared_examples_for 'sort column' do
def sort_header
find(sort_header_selector)
def column_header
find(column_header_selector)
end
it do
expect(sort_header).not_to be_nil
expect(sort_header.find("span.sort-header")[:title]).to eq(sort_text)
expect(column_header).not_to be_nil
expect(column_header.find("span.sort-header")[:title]).to eq(sort_text)
end
end
shared_examples_for 'unsorted column' do
let(:sort_text) { I18n.t(:label_sort_by, value: "\"#{link_caption}\"") }
let(:sort_text) { I18n.t(:label_open_menu) }
it_behaves_like 'sort column'
it_behaves_like 'sort column'
end
shared_examples_for 'ascending sorted column' do
let(:sort_text) { "#{I18n.t(:label_ascending)} #{I18n.t(:label_sorted_by, value: "\"#{link_caption}\"")}" }
it_behaves_like 'sort column'
it_behaves_like 'sort column'
end
shared_examples_for 'descending sorted column' do
let(:sort_text) { "#{I18n.t(:label_descending)} #{I18n.t(:label_sorted_by, value: "\"#{link_caption}\"")}" }
it_behaves_like 'sort column'
it_behaves_like 'sort column'
end
shared_examples_for 'descending sortable first' do
describe 'one click' do
before { sort_link.click }
it_behaves_like 'descending sorted column'
describe 'two clicks' do
before { sort_link.click }
it_behaves_like 'ascending sorted column'
end
shared_examples_for 'sortable column' do
describe 'Initial sort' do
it_behaves_like 'unsorted column'
end
end
shared_examples_for 'ascending sortable first' do
describe 'one click' do
before { sort_link.click }
it_behaves_like 'ascending sorted column'
describe 'descending' do
before do
column_header_link.click
sort_decending_link.click
end
describe 'two clicks' do
before { sort_link.click }
it_behaves_like 'descending sorted column'
end
it_behaves_like 'descending sorted column'
describe 'ascending' do
before do
column_header_link.click
sort_ascending_link.click
end
end
end
shared_examples_for 'sortable column' do
describe 'Initial sort' do
it_behaves_like 'unsorted column'
it_behaves_like 'ascending sorted column'
end
end
describe 'id column' do
let(:link_caption) { '#' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
describe 'type column' do
let(:link_caption) { 'Type' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
describe 'status column' do
let(:link_caption) { 'Status' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
describe 'priority column' do
let(:link_caption) { 'Priority' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
describe 'subject column' do
let(:link_caption) { 'Subject' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
describe 'assigned to column' do
let(:link_caption) { 'Assignee' }
let(:sort_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th + th + th' }
let(:sort_link_selector) { sort_header_selector + ' a' }
let(:column_header_selector) { 'table.workpackages-table th.checkbox + th + th + th + th + th + th + th' }
let(:column_header_link_selector) { column_header_selector + ' a' }
it_behaves_like 'sortable column'
it_behaves_like 'ascending sortable first'
end
end
end

Loading…
Cancel
Save