[22851] Table Inline Create (#4290)

* Move the WP create button to the WP buttons directory

* Move the keepTab service to the wp-panels directory

* Move angular module definitions into separate file

* Move the WP create button into a separate file

* Use TS in WP create button directive

* Refactor the WP create button controller

* Move the has-context-menu directive to the components directory

* Refactor has-context-menu item focus

* Create new button directive and place in table

* Export and use wpButton module

* Add title to form properties

* Add -new modifier to new work package rows

* Add WP inline create directive

* Reduce error widths to cell span

* Save or update work package conditionally

* Mark work package as new

* Implement the WP inline create button

* Style button correctly

* Decide when to activate the field for new WPs

* Remove padding on -new row

* Avoid animation on added -new rows

* Avoid writing title when passed from new workPackage

* Remove jqMigrate warning

* Avoid animations for newly added rows

* Add cancel button to inline create row

* Track new work packages by custom id

If we don't track new work packages, ng-animate will freak out due to
the track by expression. We thus track work packages by setting a custom id set from the
current date.

* Disable focus on errors outside accessibility mode

* Add inline create cancel context menu

* Conditionally set focus on fields

Set focus on the first active field
pull/4333/head
Oliver Günther 9 years ago
parent 74e01bf7d9
commit 02cf2b0efe
  1. 10
      app/assets/stylesheets/content/_tables.sass
  2. 10
      app/assets/stylesheets/content/_work_packages_table.sass
  3. 67
      app/assets/stylesheets/content/_work_packages_table_create.sass
  4. 86
      app/assets/stylesheets/content/_work_packages_table_edit.sass
  5. 1
      app/assets/stylesheets/default.css.sass
  6. 3
      config/locales/js-en.yml
  7. 172
      frontend/app/angular-modules.ts
  8. 2
      frontend/app/components/api/api-v3/hal-resources/hal-resource.service.ts
  9. 67
      frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts
  10. 30
      frontend/app/components/api/api-work-packages/api-work-packages.service.ts
  11. 97
      frontend/app/components/context-menus/has-dropdown-menu/has-dropdown-menu-directive.js
  12. 11
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.controller.ts
  13. 10
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.service.html
  14. 11
      frontend/app/components/routing/wp-list/wp-list.controller.ts
  15. 3
      frontend/app/components/routing/wp-list/wp.list.html
  16. 2
      frontend/app/components/wp-buttons/wp-buttons.module.ts
  17. 64
      frontend/app/components/wp-buttons/wp-create-button/wp-create-button.controller.ts
  18. 3
      frontend/app/components/wp-buttons/wp-create-button/wp-create-button.directive.html
  19. 33
      frontend/app/components/wp-buttons/wp-create-button/wp-create-button.directive.ts
  20. 2
      frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.test.ts
  21. 7
      frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.ts
  22. 85
      frontend/app/components/wp-buttons/wp-inline-create-button/wp-inline-create-button.controller.ts
  23. 13
      frontend/app/components/wp-buttons/wp-inline-create-button/wp-inline-create-button.directive.html
  24. 49
      frontend/app/components/wp-buttons/wp-inline-create-button/wp-inline-create-button.directive.ts
  25. 7
      frontend/app/components/wp-buttons/wp-list-view-button/wp-list-view-button.directive.ts
  26. 11
      frontend/app/components/wp-buttons/wp-view-button/wp-view-button.directive.ts
  27. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-boolean-field.directive.html
  28. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-date-field.directive.html
  29. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-duration-field.directive.html
  30. 51
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts
  31. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-float-field.directive.html
  32. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-integer-field.directive.html
  33. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-select-field.directive.html
  34. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-text-field.directive.html
  35. 18
      frontend/app/components/wp-edit/wp-edit-form.directive.ts
  36. 0
      frontend/app/components/wp-panels/keep-tab/keep-tab.service.test.ts
  37. 2
      frontend/app/components/wp-panels/keep-tab/keep-tab.service.ts
  38. 9
      frontend/app/components/wp-panels/watchers-panel/watchers-panel.directive.test.js
  39. 36
      frontend/app/components/wp-table/wp-table.directive.html
  40. 9
      frontend/app/components/wp-table/wp-table.directive.js
  41. 6
      frontend/app/components/wp-table/wp-td/wp-td.directive.js
  42. 146
      frontend/app/openproject-app.js
  43. 8
      frontend/app/services/notifications-service.js
  44. 2
      frontend/app/typings/open-project.typings.ts
  45. 2
      frontend/app/ui_components/highlight-col-directive.js
  46. 17
      frontend/app/ui_components/index.js
  47. 18
      frontend/app/ui_components/notification-box-directive.js
  48. 6
      frontend/tests/unit/tests/directives/components/notification-box-directive-test.js
  49. 7
      frontend/tests/unit/tests/directives/components/notifications-directive-test.js
  50. 4
      frontend/typings/restangular/restangular.d.ts
  51. 14
      lib/api/decorators/link_object.rb

@ -254,12 +254,10 @@ th.hidden
&.idnt-9 td.subject
padding-left: 12.5em
#content table
tr
&.context-menu-selection
background-color: #FFFFB2
&:hover
background-color: #FFFFB2
tr.context-menu-selection
background-color: #FFFFB2
&:hover
background-color: #FFFFB2
#issue_tree
table.issues

@ -42,6 +42,16 @@
&:hover
text-decoration: none
.wp-table--cancel-create-link
.icon
position: relative
left: 0.25rem
&:before
color: $body-font-color
padding: 0 0 0 0.1rem
&:hover
text-decoration: none
table.generic-table tbody tr.issue .checkbox
overflow: visible
padding-left: 5px

@ -0,0 +1,67 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
//++
.wp-inline-create-button-row td
padding: 0
.wp-inline-create-button
border-bottom: none
a
width: 100%
padding: 0.5rem 0
display: block
line-height: 1.6
&:hover
background: #e4f7fb
.wp-inline-create--add-link
font-weight: bold
.icon::before
padding-left: 7px
padding-right: 10px
font-size: 11px
&:hover
text-decoration: none
.wp--row.-new,
tr.context-menu-selection.-new
background: #BEF3CA
&:hover
background: darken(#BEF3CA, 5%)
.wp-table--cell-span:hover
border-color: #35c53f

@ -37,45 +37,48 @@
display: table-cell
width: auto
.wp-edit-field
padding-left: 0
&:hover .wp-edit-field.-error:hover
border-color: $nm-color-error-border
.wp-edit-field
padding-left: 0
&.-active
padding: 0
&.-active
padding: 0
&.-error
&.-error
.wp-table--cell-span,
input,
select
background: $nm-color-error-background
border-color: $nm-color-error-border
&:hover
border-color: lighten($nm-color-error-border, 10%)
form
width: 100%
.-hidden-overflow
overflow: hidden
text-overflow: ellipsis
&:hover .wp-edit-field.-error:hover
border-color: $nm-color-error-border
// Editable fields cursor
.wp-table--cell-span
padding: 0 5px 0 5px
cursor: text
border-color: transparent
border-style: solid
border-radius: 2px
border-width: 1px
overflow: visible
display: inline-block
line-height: 1.6
&:hover,
&:focus
border-color: $inplace-edit--border-color
form
width: 100%
.-hidden-overflow
overflow: hidden
text-overflow: ellipsis
// Editable fields cursor
.-editable .wp-table--cell-span
padding: 0 5px 0 5px
cursor: text
border-color: transparent
border-style: solid
border-radius: 2px
border-width: 1px
overflow: visible
display: inline-block
line-height: 1.6
&:hover,
&:focus
border-color: $inplace-edit--border-color
.work-package--placeholder
padding: 0 10px 0 0px
@ -88,14 +91,15 @@
cursor: pointer
// Animations on leaving work packages
.wp--row.ng-leave
-webkit-transition: all 1s ease-in-out
-moz-transition: all 1s ease-in-out
-o-transition: all 1s ease-in-out
transition: all 1s ease-in-out
.wp--row.ng-leave-active
height: 0
line-height: 0
opacity: 0
.wp--row.-animated-leave
&.ng-leave
-webkit-transition: all 1s ease-in-out
-moz-transition: all 1s ease-in-out
-o-transition: all 1s ease-in-out
transition: all 1s ease-in-out
&.ng-leave-active
height: 0
line-height: 0
opacity: 0

@ -70,6 +70,7 @@
@import content/advanced_filters
@import content/work_packages_table
@import content/work_packages_table_edit
@import content/work_packages_table_create
@import content/attributes_key_value
@import content/attributes_group
@import content/attributes_table

@ -376,6 +376,8 @@ en:
message_successful_bulk_delete: Successfully deleted work packages.
message_successful_show_in_fullscreen: "Click here to open this work package in fullscreen view"
no_value: "No value"
inline_create:
title: 'Click here to add a new work package to this list'
create:
header: 'New Work Package'
header_with_parent: 'New work package (Child of %{type} #%{id})'
@ -412,6 +414,7 @@ en:
version: "Version"
placeholders:
default: "-"
new_label: '(new)'
description: "Click to enter description..."
query:
column_names: "Columns"

@ -0,0 +1,172 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
//++
declare var I18n:op.I18n;
// global
angular.module('openproject.uiComponents',
['ui.select', 'ui.router', 'ngSanitize', 'openproject.workPackages.services'])
.run(['$rootScope', function($rootScope){
$rootScope.I18n = I18n;
}]);
angular.module('openproject.config', []);
angular.module(
'openproject.services', [
'openproject.uiComponents',
'openproject.helpers',
'openproject.workPackages.config',
'openproject.workPackages.helpers',
'openproject.api',
'angular-cache'
]);
angular.module('openproject.helpers', ['openproject.services']);
angular
.module('openproject.models', [
'openproject.workPackages.config',
'openproject.services'
]);
angular.module('openproject.viewModels', ['openproject.services']);
// timelines
angular.module('openproject.timelines', [
'openproject.timelines.controllers',
'openproject.timelines.directives',
'openproject.uiComponents'
]);
angular.module('openproject.timelines.models', ['openproject.helpers']);
angular
.module('openproject.timelines.helpers', []);
angular.module(
'openproject.timelines.controllers', [
'openproject.timelines.models'
]);
angular.module('openproject.timelines.services', [
'openproject.timelines.models',
'openproject.timelines.helpers'
]);
angular.module('openproject.timelines.directives', [
'openproject.timelines.models',
'openproject.timelines.services',
'openproject.uiComponents',
'openproject.helpers'
]);
// work packages
angular.module('openproject.workPackages', [
'openproject.workPackages.activities',
'openproject.workPackages.controllers',
'openproject.workPackages.filters',
'openproject.workPackages.directives',
'openproject.workPackages.tabs',
'openproject.uiComponents',
'ng-context-menu',
'ngFileUpload'
]);
angular.module('openproject.workPackages.services', ['openproject.inplace-edit']);
angular.module(
'openproject.workPackages.helpers', [
'openproject.helpers',
'openproject.workPackages.services'
]);
angular.module('openproject.workPackages.filters', [
'openproject.workPackages.helpers'
]);
angular.module('openproject.workPackages.config', []);
angular.module(
'openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',
'openproject.layout',
'btford.modal'
]);
angular.module('openproject.workPackages.models', []);
angular.module(
'openproject.workPackages.directives', [
'openproject.uiComponents',
'openproject.services',
'openproject.workPackages.services',
'openproject.workPackages.models'
]);
angular.module('openproject.workPackages.tabs', []);
angular.module('openproject.workPackages.activities', []);
// messages
angular.module('openproject.messages', [
'openproject.messages.controllers'
]);
angular.module('openproject.messages.controllers', []);
// time entries
angular.module('openproject.timeEntries', [
'openproject.timeEntries.controllers'
]);
angular.module('openproject.timeEntries.controllers', []);
angular.module('openproject.layout', [
'openproject.layout.controllers',
'ui.router'
]);
angular.module('openproject.layout.controllers', []);
angular.module('openproject.api', ['restangular', 'openproject.services']);
angular.module('openproject.templates', []);
// refactoring
angular.module('openproject.inplace-edit', []);
angular.module('openproject.responsive', []);
export var wpButtonsModule =
angular.module('openproject.wpButtons', ['ui.router', 'openproject.services']);
// main app
export default angular.module('openproject', [
'ui.date',
'ui.router',
'openproject.config',
'openproject.uiComponents',
'openproject.timelines',
'openproject.workPackages',
'openproject.messages',
'openproject.timeEntries',
'ngAnimate',
'ngAria',
'ngSanitize',
'truncate',
'openproject.layout',
'cgBusy',
'openproject.api',
'openproject.templates',
'monospaced.elastic',
'openproject.inplace-edit',
'openproject.wpButtons',
'openproject.responsive'
]);

@ -72,7 +72,7 @@ function halResource($q, _, lazy, halTransform, HalLink) {
});
}
constructor(public $source, public $loaded = true) {
constructor(public $source:any, public $loaded:boolean = true) {
this.$source = $source._plain || $source;
this.proxyProperties();

@ -26,11 +26,48 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
function wpResource(HalResource:typeof op.HalResource, NotificationsService:any, $q:ng.IQService) {
function wpResource(
HalResource:typeof op.HalResource,
apiWorkPackages,
NotificationsService:any,
$q:ng.IQService
) {
class WorkPackageResource extends HalResource {
private form;
public schema;
public id;
public static fromCreateForm(projectIdentifier?:string):ng.IPromise<WorkPackageResource> {
var deferred = $q.defer();
apiWorkPackages.emptyCreateForm(projectIdentifier)
.then(resource => {
var wp = new WorkPackageResource(resource.payload.$source, true);
// Copy resources from form response
wp.schema = resource.schema;
wp.form = $q.when(resource);
wp.id = 'new-' + Date.now();
deferred.resolve(wp);
})
.catch(deferred.reject);
return deferred.promise;
}
getForm() {
public get isNew():boolean {
var id = Number(this.id);
return isNaN(id);
}
public requiredValueFor(fieldName):boolean {
var fieldSchema = this.schema[fieldName];
return !this[fieldName] && fieldSchema.writable && fieldSchema.required;
}
public getForm() {
if (!this.form) {
this.form = this.$links.update(this);
this.form.catch(error => {
@ -40,7 +77,7 @@ function wpResource(HalResource:typeof op.HalResource, NotificationsService:any,
return this.form;
}
getSchema() {
public getSchema() {
return this.getForm().then(form => {
const schema = form.$embedded.schema;
@ -56,7 +93,7 @@ function wpResource(HalResource:typeof op.HalResource, NotificationsService:any,
});
}
save() {
public save() {
const plain = this.$plain();
delete plain.createdAt;
@ -82,7 +119,7 @@ function wpResource(HalResource:typeof op.HalResource, NotificationsService:any,
}
});
return this.$links.updateImmediately(plainPayload)
return this.saveResource(plainPayload)
.then(workPackage => {
angular.extend(this, workPackage);
@ -90,7 +127,13 @@ function wpResource(HalResource:typeof op.HalResource, NotificationsService:any,
}).catch((error) => {
deferred.reject(error);
}).finally(() => {
this.form = null;
// Restore the form for subsequent saves
// e.g., due to changes in lockVersion.
// Not needed for inline create.
if (!this.isNew) {
this.form = null;
}
});
});
@ -101,13 +144,21 @@ function wpResource(HalResource:typeof op.HalResource, NotificationsService:any,
return !(this as any).children;
}
isParentOf(otherWorkPackage) {
public isParentOf(otherWorkPackage) {
return otherWorkPackage.parent.$links.self.$link.href ===
this.$links.self.$link.href;
}
public get isEditable():boolean {
return !!this.$links.update;
return !!this.$links.update || this.isNew;
}
protected saveResource(payload):ng.IPromise<any> {
if (this.isNew) {
return apiWorkPackages.wpApiPath().post(payload);
} else {
return this.$links.updateImmediately(payload);
}
}
}

@ -27,21 +27,18 @@
//++
export class ApiWorkPackagesService {
protected wpBaseApi;
constructor (protected DEFAULT_PAGINATION_OPTIONS,
protected $stateParams,
protected $q:ng.IQService,
protected apiV3:restangular.IService) {
this.wpBaseApi = apiV3.service('work_packages');
}
public list(offset:number, pageSize:number, query:api.ex.Query) {
var workPackages;
if (query.projectId) {
workPackages = this.apiV3.service('work_packages', this.apiV3.one('projects', query.projectId));
}
else {
workPackages = this.apiV3.service('work_packages');
}
var workPackages = this.wpApiPath(query.projectId);
return workPackages.getList(
this.queryAsV3Params(offset, pageSize, query),
@ -51,6 +48,23 @@ export class ApiWorkPackagesService {
);
}
/**
* Returns a promise to post `/api/v3/work_packages/form`.
*
* @returns An empty work package form resource.
*/
public emptyCreateForm(projectIdentifier?:string):ng.IPromise<op.HalResource> {
return this.wpApiPath(projectIdentifier).one('form').customPOST();
}
public wpApiPath(projectIdentifier?: any) {
if (!!projectIdentifier) {
return this.apiV3.service('work_packages', this.apiV3.one('projects', projectIdentifier));
} else {
return this.apiV3.service('work_packages');
}
}
protected queryAsV3Params(offset:number, pageSize:number, query:api.ex.Query) {
const v3Filters = _.map(query.filters, (filter:any) => {

@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,15 +24,14 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
// ++
function hasDropdownMenu($rootScope, $injector, $window, FocusHelper) {
function getCssPositionProperties(dropdown, trigger) {
var hOffset = 0,
vOffset = 0;
if (dropdown.hasClass('dropdown-anchor-top')) {
vOffset = - dropdown.outerHeight() - trigger.outerHeight() + parseInt(trigger.css('margin-top'), 10);
vOffset = -dropdown.outerHeight() - trigger.outerHeight() + parseInt(trigger.css('margin-top'), 10);
}
// Styling logic taken from jQuery-dropdown plugin: https://github.com/plapier/jquery-dropdown
@ -42,30 +41,32 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
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'), 10) + hOffset :
trigger.position().left + parseInt(trigger.css('margin-left'), 10) + hOffset,
trigger.position().left -
(dropdown.outerWidth(true) - trigger.outerWidth(true)) -
parseInt(trigger.css('margin-right'), 10) + hOffset :
trigger.position().left + parseInt(trigger.css('margin-left'), 10) + hOffset,
top: trigger.position().top +
trigger.outerHeight(true) -
parseInt(trigger.css('margin-top'), 10) + vOffset
trigger.outerHeight(true) -
parseInt(trigger.css('margin-top'), 10) + vOffset
};
} else {
}
else {
return {
left: dropdown.hasClass('dropdown-anchor-right') ?
trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth()) + hOffset : trigger.offset().left + hOffset,
trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth()) + hOffset : trigger.offset().left + hOffset,
top: trigger.offset().top + trigger.outerHeight() + vOffset
};
}
}
function getPositionPropertiesOfEvent(event) {
var position = { };
var position = {};
if (event.pageX && event.pageY) {
position.top = Math.max(event.pageY, 0);
position.left = Math.max(event.pageX, 0);
} else {
}
else {
var bounding = angular.element(event.target)[0].getBoundingClientRect();
position.top = Math.max(bounding.bottom, 0);
@ -86,20 +87,20 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
return {
restrict: 'A',
controller: [function() {
controller: [function () {
var dropDownMenuOpened = false;
this.open = function() {
this.open = function () {
dropDownMenuOpened = true;
};
this.close = function() {
this.close = function () {
dropDownMenuOpened = false;
};
this.opened = function() {
this.opened = function () {
return dropDownMenuOpened;
};
}],
link: function(scope, element, attrs, ctrl) {
link: function (scope, element, attrs, ctrl) {
var contextMenu = $injector.get(attrs.target),
locals = {},
pointerPosition,
@ -111,12 +112,16 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
triggerOnEvent = (attrs.triggerOnEvent || 'click') + '.dropdown.openproject';
/* 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" */
menu to the trigger event
triggerOnEvent allows for binding the event for opening the menu to "click" */
function toggle(event) {
active() ? close() : open(event);
if (active()) {
close();
} else {
open(event);
}
}
function active() {
@ -129,21 +134,21 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
pointerCssPosition = getCssPositionPropertiesOfEvent(event);
$rootScope.$broadcast('openproject.dropdown.closeDropdowns', ignoreFocusOpener);
// prepare locals, these define properties to be passed on to the context menu scope
var localKeys = (attrs.locals || '').split(',').map(function(local) {
var localKeys = (attrs.locals || '').split(',').map(function (local) {
return local.trim();
});
angular.forEach(localKeys, function(key) {
angular.forEach(localKeys, function (key) {
locals[key] = scope[key];
});
ctrl.open();
contextMenu.open(element, locals)
.then(function(element) {
.then(function (element) {
menuElement = element;
menuElement.trap();
positionDropdown();
menuElement.on('click', function(e) {
menuElement.on('click', function (e) {
// allow inputs to be clickable
// without closing the dropdown
if (angular.element(e.target).is(':input')) {
@ -156,7 +161,7 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
function close(ignoreFocusOpener) {
ctrl.close();
var disableFocus = ignoreFocusOpener;
contextMenu.close(disableFocus).then(function() {
contextMenu.close(disableFocus).then(function () {
if (!ignoreFocusOpener) {
FocusHelper.focusElement(afterFocusOn ? element.find(afterFocusOn) : element);
}
@ -166,7 +171,7 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
function positionDropdown() {
var positionRelativeToElement = positionRelativeTo ?
element.find(positionRelativeTo) : element;
if (attrs.triggerOnEvent == 'contextmenu') {
if (attrs.triggerOnEvent === 'contextmenu') {
menuElement.css(pointerCssPosition);
adjustPosition(menuElement, pointerPosition);
} else {
@ -176,8 +181,8 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
function adjustPosition($element, pointerPosition) {
var viewport = {
top : win.scrollTop(),
left : win.scrollLeft()
top: win.scrollTop(),
left: win.scrollLeft()
};
viewport.right = viewport.left + win.width();
@ -193,25 +198,27 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
}
}
element.bind(triggerOnEvent, function(event) {
element.bind(triggerOnEvent, function (event) {
event.preventDefault();
event.stopPropagation();
scope.$apply(function() {
scope.$apply(function () {
toggle(event);
});
menuElement.find('.menu-item').first().focus();
// set css position parameters after the digest has been completed
if (contextMenu.active()) positionDropdown();
});
scope.$on('openproject.dropdown.closeDropdowns', function(event, ignoreFocusOpener) {
scope.$on('openproject.dropdown.closeDropdowns', function (event, ignoreFocusOpener) {
if (!ctrl.opened()) {
return;
}
close(ignoreFocusOpener);
});
scope.$on('openproject.dropdown.reposition', function() {
scope.$on('openproject.dropdown.reposition', function () {
if (contextMenu.active() && menuElement && ctrl.opened()) {
positionDropdown();
}
@ -220,14 +227,12 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
var elementKeyUpString = 'keyup.contextmenu.dropdown.openproject';
element
.off(elementKeyUpString)
.on(elementKeyUpString, function(event) {
// Alt + Shift + F10
if (event.keyCode === 121 && event.shiftKey && event.altKey) {
if (!contextMenu.active()) {
.on(elementKeyUpString, function (event) {
// Alt + Shift + F10
if (event.keyCode === 121 && event.shiftKey && event.altKey && !contextMenu.active()) {
open(event);
}
}
});
});
// We need the off/on stuff in order to not have a new listener
@ -241,13 +246,13 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
var repositioningEventString = 'resize.dropdown.openproject, mousewheel.dropdown.openproject';
win
.off(repositioningEventString)
.on(repositioningEventString, function() {
.on(repositioningEventString, function () {
$rootScope.$broadcast('openproject.dropdown.reposition');
});
var keyUpEventString = 'keyup.dropdown.openproject';
win
.off(keyUpEventString).on(keyUpEventString, function(event) {
.off(keyUpEventString).on(keyUpEventString, function (event) {
if (event.keyCode === 27) {
$rootScope.$broadcast('openproject.dropdown.closeDropdowns');
}
@ -268,4 +273,8 @@ module.exports = function($rootScope, $injector, $window, $parse, FocusHelper) {
.on(triggerOnEvent, handleWindowClickEvent);
}
};
};
}
angular
.module('openproject.uiComponents')
.directive('hasDropdownMenu', hasDropdownMenu);

@ -28,6 +28,7 @@
function wpContextMenuController(
$scope,
$rootScope,
$state,
WorkPackagesTableHelper,
WorkPackageContextMenuHelper,
@ -71,6 +72,16 @@ function wpContextMenuController(
}
};
$scope.cancelInlineCreate = function(index, row) {
$rootScope.$emit('inlineWorkPackageCreateCancelled', index, row);
emitClosingEvents();
}
function emitClosingEvents() {
$scope.$emit('hideAllDropdowns');
$scope.$root.$broadcast('openproject.dropdown.closeDropdowns', true);
}
function deleteSelectedWorkPackages() {
var ids = getSelectedWorkPackages().map(function(wp) { return wp.id; });

@ -1,12 +1,18 @@
<div id="work-package-context-menu" class="action-menu dropdown" role="menu">
<ul class="dropdown-menu">
<li class="open detailsViewMenuItem">
<li ng-if="row.object.isNew" class="open detailsViewMenuItem">
<a role="menuitem" focus ng-click="cancelInlineCreate($index, row)">
<i ng-class="['icon-action-menu', 'icon-delete']"></i>
<span ng-bind="I18n.t('js.button_cancel')"/>
</a>
</li>
<li ng-if="!row.object.isNew" class="open detailsViewMenuItem">
<a role="menuitem" focus ui-sref="work-packages.list.details.overview({workPackageId: row.object.id})">
<i ng-class="['icon-action-menu', 'icon-view-split']"></i>
<span ng-bind="I18n.t('js.button_open_details')"/>
</a>
</li>
<li class="openFullScreenView">
<li ng-if="!row.object.isNew" class="openFullScreenView">
<a role="menuitem" ui-sref="work-packages.show({workPackageId: row.object.id})">
<i ng-class="['icon-action-menu', 'icon-view-fullscreen']"></i>
<span ng-bind="I18n.t('js.button_open_fullscreen')"/>

@ -46,6 +46,7 @@ function WorkPackagesListController($scope,
$scope.projectIdentifier = $state.params.projectPath || null;
$scope.loadingIndicator = loadingIndicator;
$scope.I18n = I18n;
// Setup
function initialSetup() {
@ -145,7 +146,15 @@ function WorkPackagesListController($scope,
// yield updatable data to scope
$scope.columns = $scope.query.columns;
$scope.rows = WorkPackagesTableService.getRows();
// Merge new row if it exists
var newRows = WorkPackagesTableService.getRows();
var last = <any> _.last($scope.rows);
if (last && last.object.isNew) {
newRows.push(last);
}
$scope.rows = newRows;
$scope.groupableColumns = WorkPackagesTableService.getGroupableColumns();
$scope.totalEntries = QueryService.getTotalEntries();
$scope.resource = json.resource;

@ -51,8 +51,7 @@
class="button last work-packages-settings-button"
has-dropdown-menu
target="SettingsDropdownMenu"
locals="query"
onclick="setTimeout(function(){ $(document).getElementsByClassName('menu-item')[0].focus(); }, 100);">
locals="query">
<i class="button--icon icon-show-more"></i>
</button>
</li>

@ -35,7 +35,6 @@ interface ButtonControllerText {
buttonText: string;
}
export abstract class WorkPackageButtonController {
public disabled:boolean;
public buttonId:string;
@ -77,6 +76,7 @@ export abstract class WorkPackageButtonController {
}
public abstract isActive():boolean;
public abstract performAction();
}

@ -0,0 +1,64 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
// ++
import {wpButtonsModule} from '../../../angular-modules';
export default class WorkPackageCreateButtonController {
public projectIdentifier:string;
public text:any;
public types:any;
protected canCreate:boolean = false;
public get inProjectContext() {
return !!this.projectIdentifier;
}
constructor(protected $state, protected I18n, protected ProjectService) {
this.text = {
button: I18n.t('js.label_work_package'),
create: I18n.t('js.label_create_work_package')
};
if (this.inProjectContext) {
this.ProjectService.fetchProjectResource(this.projectIdentifier).then(project => {
this.canCreate = !!project.links.createWorkPackage;
});
this.ProjectService.getProject(this.projectIdentifier).then(project => {
this.types = project.embedded.types;
});
}
}
public isDisabled() {
return !this.inProjectContext || !this.canCreate || this.$state.includes('**.new') || !this.types;
}
}
wpButtonsModule.controller('WorkPackageCreateButtonController', WorkPackageCreateButtonController);

@ -1,8 +1,7 @@
<div class="wp-create-button">
<button class="button -alt-highlight add-work-package" has-dropdown-menu
target="typesDropDownMenu" locals="vm" ng-disabled="vm.isDisabled()"
aria-label="{{ ::vm.text.create }}" aria-haspopup="true"
onclick="setTimeout(function(){ $(document).getElementsByClassName('menu-item')[0].focus(); }, 100);">
aria-label="{{ ::vm.text.create }}" aria-haspopup="true">
<i class="button--icon icon-add"></i>
<span class="button--text" ng-bind="::vm.text.button" aria-hidden="true"></span>

@ -26,14 +26,12 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
angular
.module('openproject.workPackages')
.directive('wpCreateButton', wpCreateButton);
import {wpButtonsModule} from '../../../angular-modules';
function wpCreateButton() {
return {
restrict: 'E',
templateUrl: '/components/work-packages/wp-create-button/wp-create-button.directive.html',
templateUrl: '/components/wp-buttons/wp-create-button/wp-create-button.directive.html',
scope: {
projectIdentifier: '=',
@ -42,31 +40,8 @@ function wpCreateButton() {
bindToController: true,
controllerAs: 'vm',
controller: WorkPackageCreateButtonController
controller: 'WorkPackageCreateButtonController'
}
}
function WorkPackageCreateButtonController($state, ProjectService) {
var vm = this,
inProjectContext = !!vm.projectIdentifier,
canCreate= false;
vm.text = {
button: I18n.t('js.label_work_package'),
create: I18n.t('js.label_create_work_package')
};
vm.isDisabled = function () {
return !inProjectContext || !canCreate || $state.includes('**.new') || !vm.types;
};
if (inProjectContext) {
ProjectService.fetchProjectResource(vm.projectIdentifier).then(function(project) {
canCreate = !!project.links.createWorkPackage;
});
ProjectService.getProject(vm.projectIdentifier).then(function (project) {
vm.types = project.embedded.types;
});
}
}
wpButtonsModule.directive('wpCreateButton', wpCreateButton);

@ -27,7 +27,7 @@
// ++
import {WorkPackageDetailsViewButtonController} from './wp-details-view-button.directive';
import {KeepTabService} from "../keep-tab/keep-tab.service";
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
var expect = chai.expect;

@ -26,8 +26,9 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {wpButtonsModule} from '../../../angular-modules';
import {WorkPackageNavigationButtonController, wpButtonDirective} from '../wp-buttons.module';
import {KeepTabService} from '../keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
export class WorkPackageDetailsViewButtonController extends WorkPackageNavigationButtonController {
public projectIdentifier:number;
@ -73,6 +74,4 @@ function wpDetailsViewButton() {
});
}
angular
.module('openproject.wpButtons')
.directive('wpDetailsViewButton', wpDetailsViewButton);
wpButtonsModule.directive('wpDetailsViewButton', wpDetailsViewButton);

@ -0,0 +1,85 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
// ++
import {wpButtonsModule} from '../../../angular-modules';
import WorkPackageCreateButtonController from '../wp-create-button/wp-create-button.controller';
class WorkPackageInlineCreateButtonController extends WorkPackageCreateButtonController {
public columns:any[];
public rows:any[];
public hidden:boolean = false;
private _wp;
constructor(
protected $state,
protected $rootScope,
protected $element,
protected I18n,
protected ProjectService,
protected WorkPackageResource,
protected apiWorkPackages
) {
super($state, I18n, ProjectService);
$rootScope.$on('workPackageSaved', (_event, savedWp) => {
// Add another row
if (savedWp === this._wp) {
this.addWorkPackageRow();
}
});
$rootScope.$on('inlineWorkPackageCreateCancelled', (_event, index, row) => {
if (row.object === this._wp) {
this.rows.splice(index, 1);
this.show();
}
});
}
public addWorkPackageRow() {
this.WorkPackageResource.fromCreateForm(this.projectIdentifier).then(wp => {
this._wp = wp;
wp.inlineCreated = true;
this.rows.push({ level: 0, ancestors: [], object: wp, parent: undefined });
this.hide();
});
}
public hide() {
return this.hidden = true;
}
public show() {
return this.hidden = false;
}
}
wpButtonsModule.controller(
'WorkPackageInlineCreateButtonController', WorkPackageInlineCreateButtonController);

@ -0,0 +1,13 @@
<div
class="wp-inline-create-button"
ng-hide="$ctrl.hidden || $ctrl.isDisabled() || !$ctrl.canCreate">
<a class="wp-inline-create--add-link"
ng-click="$ctrl.addWorkPackageRow()"
ng-disabled="$ctrl.isDisabled()"
aria-label="{{ ::$ctrl.text.create }}"
aria-haspopup="true">
<i class="icon icon-add"></i>
<span ng-bind="::$ctrl.text.button" aria-hidden="true"></span>
</a>
</div>

@ -0,0 +1,49 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
// ++
function wpInlineCreateButton() {
return {
restrict: 'AE',
templateUrl: '/components/wp-buttons/wp-inline-create-button/' +
'wp-inline-create-button.directive.html',
scope: {
projectIdentifier: '=',
rows: '=',
columns: '='
},
bindToController: true,
controllerAs: '$ctrl',
controller: 'WorkPackageInlineCreateButtonController'
}
}
angular
.module('openproject.wpButtons')
.directive('wpInlineCreateButton', wpInlineCreateButton);

@ -26,6 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {wpButtonsModule} from '../../../angular-modules';
import {WorkPackageNavigationButtonController, wpButtonDirective} from '../wp-buttons.module';
export class WorkPackageListViewButtonController extends WorkPackageNavigationButtonController {
@ -66,7 +67,7 @@ export class WorkPackageListViewButtonController extends WorkPackageNavigationBu
}
}
function wpListViewButton(): ng.IDirective {
function wpListViewButton():ng.IDirective {
return wpButtonDirective({
scope: {
projectIdentifier: '=',
@ -77,6 +78,4 @@ function wpListViewButton(): ng.IDirective {
});
}
angular
.module('openproject.wpButtons')
.directive('wpListViewButton', wpListViewButton);
wpButtonsModule.directive('wpListViewButton', wpListViewButton);

@ -26,10 +26,11 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {wpButtonsModule} from '../../../angular-modules';
import {WorkPackageNavigationButtonController, wpButtonDirective} from '../wp-buttons.module';
import {KeepTabService} from '../keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
export class WorkPackageViewButtonController extends WorkPackageNavigationButtonController{
export class WorkPackageViewButtonController extends WorkPackageNavigationButtonController {
public workPackageId:number;
public nextWpFunc:Function;
@ -65,7 +66,7 @@ export class WorkPackageViewButtonController extends WorkPackageNavigationButton
}
}
function wpViewButton(): ng.IDirective {
function wpViewButton():ng.IDirective {
return wpButtonDirective({
scope: {
workPackageId: '=?',
@ -76,6 +77,4 @@ function wpViewButton(): ng.IDirective {
});
}
angular
.module('openproject.wpButtons')
.directive('wpViewButton', wpViewButton);
wpButtonsModule.directive('wpViewButton', wpViewButton);

@ -1,6 +1,7 @@
<input type="checkbox"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-model="vm.workPackage[vm.fieldName]"
ng-change="vm.submit()"
ng-blur="vm.deactivate()"
focus />
focus="vm.shouldFocus()" />

@ -5,6 +5,7 @@
<input ng-model="vm.workPackage[vm.fieldName]"
type="text"
focus/>
class="wp-inline-edit--field"
focus="vm.shouldFocus()" />
</op-date-picker>

@ -1,6 +1,7 @@
<input type="number"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-model="vm.workPackage[vm.fieldName]"
duration-value
ng-blur="vm.deactivate()"
focus>
focus="vm.shouldFocus()">

@ -35,6 +35,7 @@ export class WorkPackageEditFieldController {
public formCtrl: WorkPackageEditFormController;
public wpEditForm:ng.IFormController;
public fieldName:string;
public fieldIndex:number;
public field:Field;
public errorenous:boolean;
protected pristineValue:any;
@ -50,6 +51,7 @@ export class WorkPackageEditFieldController {
protected $element,
protected NotificationsService,
protected I18n) {
}
public get workPackage() {
@ -65,13 +67,14 @@ export class WorkPackageEditFieldController {
.then(() => this.deactivate());
}
public activate() {
public activate(forceFocus = true) {
if (this._active) {
this.setFocus(forceFocus);
return;
}
this.pristineValue = angular.copy(this.workPackage[this.fieldName]);
this.setupField().then(() => {
this.buildEditField().then(() => {
this._active = this.field.schema.writable;
// Display a generic error if the field turns out not to be editable,
@ -82,6 +85,27 @@ export class WorkPackageEditFieldController {
{ attribute: this.field.schema.name }
));
}
this.setFocus(forceFocus);
});
}
public initializeField() {
// Activate field when creating a work package
// and the schema requires this field
if (this.workPackage.isNew && this.workPackage.requiredValueFor(this.fieldName)) {
this.activate();
var activeField = this.formCtrl.firstActiveField;
if (!activeField || this.formCtrl.fields[activeField].fieldIndex > this.fieldIndex) {
this.formCtrl.firstActiveField = this.fieldName;
}
}
// Mark the td field if it is inline-editable
// We're resolving the non-form schema here since its loaded anyway for the table
this.workPackage.schema.$load().then(schema => {
this.editable = schema[this.fieldName].writable;
});
}
@ -94,6 +118,16 @@ export class WorkPackageEditFieldController {
this.$element.toggleClass('-editable', enabled);
}
public shouldFocus() {
return !this.workPackage.isNew || this.formCtrl.firstActiveField === this.fieldName;
}
public setFocus(focus = true) {
if (focus) {
this.$element.find('.wp-inline-edit--field').focus();
}
}
public deactivate():boolean {
return this._active = false;
}
@ -111,12 +145,13 @@ export class WorkPackageEditFieldController {
this.pristineValue = null;
}
protected setupField():ng.IPromise<any> {
protected buildEditField():ng.IPromise<any> {
return this.formCtrl.loadSchema().then(schema => {
this.field = this.wpEditField.getField(
this.workPackage, this.fieldName, schema[this.fieldName]);
});
}
}
function wpEditFieldLink(
@ -128,11 +163,7 @@ function wpEditFieldLink(
controllers[1].formCtrl = controllers[0];
controllers[1].formCtrl.fields[scope.vm.fieldName] = scope.vm;
// Mark the td field if it is inline-editable
// We're resolving the non-form schema here since its loaded anyway for the table
scope.vm.workPackage.schema.$load().then(schema => {
scope.vm.editable = schema[scope.vm.fieldName].writable;
});
scope.vm.initializeField();
element.addClass(scope.vm.fieldName);
element.keyup(event => {
@ -157,7 +188,9 @@ function wpEditField() {
transclude: true,
scope: {
fieldName: '=wpEditField'
fieldName: '=wpEditField',
fieldIndex: '=fieldIndex',
columns: '=columns'
},
require: ['^wpEditForm', 'wpEditField'],

@ -1,6 +1,7 @@
<input type="text"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-model="vm.workPackage[vm.fieldName]"
ng-blur="vm.deactivate()"
float-value
focus />
focus="vm.shouldFocus()" />

@ -1,5 +1,6 @@
<input type="number"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-model="vm.workPackage[vm.fieldName]"
ng-blur="vm.deactivate()"
focus>
focus="vm.shouldFocus()">

@ -1,8 +1,9 @@
<select ng-model="vm.workPackage[vm.fieldName]"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-options="value as (value.name || value.value) for value in vm.field.options track by value.href"
ng-change="vm.submit()"
ng-blur="vm.deactivate()"
focus>
focus="vm.shouldFocus()">
<option value="" ng-if="false"></option>
</select>

@ -1,5 +1,6 @@
<input type="text"
class="wp-inline-edit--field"
wp-edit-field-requirements="vm.field.schema"
ng-model="vm.workPackage[vm.fieldName]"
ng-blur="vm.deactivate()"
focus>
focus="vm.shouldFocus()">

@ -29,6 +29,7 @@
export class WorkPackageEditFormController {
public workPackage;
public fields = {};
public firstActiveField:string;
constructor(
protected NotificationsService,
@ -38,6 +39,12 @@ export class WorkPackageEditFormController {
protected $timeout) {
}
public isFieldRequired(fieldName) {
return _.filter((this.fields as any), (name:string, _field) => {
return !this.workPackage[name] && this.workPackage.requiredValueFor(name);
});
}
public loadSchema() {
return this.workPackage.getSchema();
}
@ -45,11 +52,16 @@ export class WorkPackageEditFormController {
public updateWorkPackage() {
var deferred = this.$q.defer();
// Reset old error notifcations
this.$rootScope.$emit('notifications.clearAll');
this.workPackage.save()
.then(() => {
angular.forEach(this.fields, field => field.setErrorState(false));
deferred.resolve();
this.$rootScope.$emit('workPackagesRefreshInBackground');
this.$rootScope.$emit('workPackageSaved', this.workPackage);
this.$rootScope.$emit('workPackagesRefreshInBackground');
})
.catch((error) => {
if (!error.data) {
@ -85,6 +97,10 @@ export class WorkPackageEditFormController {
angular.forEach(this.fields, (field) => {
field.setErrorState(columns.indexOf(field.fieldName) !== -1);
});
// Activate + Focus on first field
this.firstActiveField = columns[0];
this.fields[this.firstActiveField].activate(true);
});
}
}

@ -59,5 +59,5 @@ export class KeepTabService {
}
angular
.module('openproject.wpButtons')
.module('openproject.workPackages.services')
.service('keepTab', KeepTabService);

@ -29,6 +29,15 @@
describe('Watchers panel directive', function () {
var $compile, $rootScope, element;
beforeEach(angular.mock.module('openproject.services', function($provide) {
var configurationService = {};
configurationService.accessibilityModeEnabled = sinon.stub().returns(false);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(angular.mock.module('openproject.workPackages.controllers', function ($controllerProvider) {
$controllerProvider.register('WatchersPanelController', function () {});
}));

@ -86,23 +86,25 @@
has-dropdown-menu
trigger-on-event="contextmenu"
target="WorkPackageContextMenu"
locals="rows,row"
locals="rows,row,$index"
after-focus-on=".id :focusable"
single-click="selectWorkPackage(row, $event)"
ng-dblclick="openWorkPackageInFullView(row)"
single-click="!row.object.isNew && selectWorkPackage(row, $event)"
ng-dblclick="!row.object.isNew && openWorkPackageInFullView(row)"
ng-class="[
'issue',
'hascontextmenu',
row.checked && 'context-menu-selection',
!row.object['leaf?'] && 'parent' || '',
row.level > 0 && 'child idnt' || '',
row.level > 0 && ('idnt-' + row.level) || ''
row.level > 0 && ('idnt-' + row.level) || '',
row.object.isNew && '-new' || '',
!(row.object.isNew || row.object.inlineCreate) && '-animated' || '',
]"
ng-show="!groupByColumn || groupExpanded[row.groupName]"
wp-edit-form="row.object">
<td class="checkbox -short hide-when-print">
<td ng-if="!row.object.isNew" class="checkbox -short hide-when-print">
<accessible-checkbox name="ids[]"
checkbox-id="work_package{{row.object.id}}"
checkbox-value="row.object.id"
@ -117,10 +119,21 @@
</a>
</span>
</td>
<td ng-if="row.object.isNew" class="cancel-inline-create -short">
<span>
<a class="wp-table--cancel-create-link"
ng-click="cancelInlineWorkPackage($index, row)">
<i class="icon icon-delete"></i>
<span ng-bind="I18n.t('js.button_cancel')"/>
</a>
</span>
</td>
<td ng-repeat="column in columns"
lang="{{column.custom_field && column.custom_field.name_locale || locale}}"
wp-edit-field="column.name"
columns="columns"
field-index="$index"
class="wp-table--cell"
ng-class="{ '-short': column.name == 'id' }">
<wp-td attribute="column.name"
@ -158,9 +171,19 @@
</wp-td>
</td>
</tr>
<tr wp-inline-create-row></tr>
</tbody>
<tfoot>
<!-- Inline create button -->
<tr class="wp-inline-create-button-row">
<!-- Add 2 to the colspan attr because of the id and the checkbox columns -->
<td colspan="{{ columns.length + 2 }}">
<wp-inline-create-button
project-identifier="projectIdentifier"
rows="rows"
></wp-inline-create-button>
</td>
</tr>
<!-- Total sums -->
@ -179,7 +202,6 @@
</wp-td>
</td>
</tr>
</tfoot>
</table>
<div class="generic-table--header-background"></div>

@ -42,6 +42,7 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
query: '=',
groupBy: '=',
displaySums: '=',
isSmaller: '=',
resource: '=',
activationCallback: '&'
},
@ -193,14 +194,14 @@ function wpTable(WorkPackagesTableService, $window, PathHelper, apiWorkPackages,
scope.setCheckedStateForAllRows(false);
setRowSelectionState(row, true);
scope.activationCallback({ id: row.object.id, force: true });
};
}
};
}
function WorkPackagesTableController($scope) {
function WorkPackagesTableController($scope, $rootScope) {
$scope.locale = I18n.locale;
$scope.text = {
@ -216,4 +217,8 @@ function WorkPackagesTableController($scope) {
$scope.text.toggleRows =
checked ? I18n.t('js.button_uncheck_all') : I18n.t('js.button_check_all');
});
$scope.cancelInlineWorkPackage = function (index, row) {
$rootScope.$emit('inlineWorkPackageCreateCancelled', index, row);
}
}

@ -71,6 +71,12 @@ function WorkPackageTdController($scope, I18n, PathHelper, WorkPackagesHelper) {
return;
}
if (vm.object.isNew && vm.attribute === 'id') {
vm.displayText = 'text';
vm.displayText = I18n.t('js.work_packages.placeholders.new_label');
return;
}
if (!vm.object[vm.attribute] ) {
vm.displayText = I18n.t('js.work_packages.placeholders.default');
return;

@ -62,151 +62,11 @@ require('angular-cache');
require('mousetrap');
require('ngFileUpload');
// global
angular.module('openproject.uiComponents',
['ui.select', 'ui.router', 'ngSanitize', 'openproject.workPackages.services'])
.run(['$rootScope', function($rootScope){
$rootScope.I18n = I18n;
}]);
angular.module('openproject.config', []);
angular.module(
'openproject.services', [
'openproject.uiComponents',
'openproject.helpers',
'openproject.workPackages.config',
'openproject.workPackages.helpers',
'openproject.api',
'angular-cache'
]);
angular.module('openproject.helpers', ['openproject.services']);
angular
.module('openproject.models', [
'openproject.workPackages.config',
'openproject.services'
]);
angular.module('openproject.viewModels', ['openproject.services']);
// timelines
angular.module('openproject.timelines', [
'openproject.timelines.controllers',
'openproject.timelines.directives',
'openproject.uiComponents'
]);
angular.module('openproject.timelines.models', ['openproject.helpers']);
angular
.module('openproject.timelines.helpers', []);
angular.module(
'openproject.timelines.controllers', [
'openproject.timelines.models'
]);
angular.module('openproject.timelines.services', [
'openproject.timelines.models',
'openproject.timelines.helpers'
]);
angular.module('openproject.timelines.directives', [
'openproject.timelines.models',
'openproject.timelines.services',
'openproject.uiComponents',
'openproject.helpers'
]);
// work packages
angular.module('openproject.workPackages', [
'openproject.workPackages.activities',
'openproject.workPackages.controllers',
'openproject.workPackages.filters',
'openproject.workPackages.directives',
'openproject.workPackages.tabs',
'openproject.uiComponents',
'ng-context-menu',
'ngFileUpload'
]);
angular.module('openproject.workPackages.services', ['openproject.inplace-edit']);
angular.module(
'openproject.workPackages.helpers', [
'openproject.helpers',
'openproject.workPackages.services'
]);
angular.module('openproject.workPackages.filters', [
'openproject.workPackages.helpers'
]);
angular.module('openproject.workPackages.config', []);
angular.module(
'openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',
'openproject.layout',
'btford.modal'
]);
angular.module('openproject.workPackages.models', []);
angular.module(
'openproject.workPackages.directives', [
'openproject.uiComponents',
'openproject.services',
'openproject.workPackages.services',
'openproject.workPackages.models'
]);
angular.module('openproject.workPackages.tabs', []);
angular.module('openproject.workPackages.activities', []);
// messages
angular.module('openproject.messages', [
'openproject.messages.controllers'
]);
angular.module('openproject.messages.controllers', []);
// time entries
angular.module('openproject.timeEntries', [
'openproject.timeEntries.controllers'
]);
angular.module('openproject.timeEntries.controllers', []);
angular.module('openproject.layout', [
'openproject.layout.controllers',
'ui.router'
]);
angular.module('openproject.layout.controllers', []);
angular.module('openproject.api', ['restangular', 'openproject.services']);
angular.module('openproject.templates', []);
// refactoring
angular.module('openproject.inplace-edit', []);
angular.module('openproject.wpButtons', ['ui.router', 'openproject.services']);
angular.module('openproject.responsive', []);
// main app
var openprojectApp = angular.module('openproject', [
'ui.date',
'ui.router',
'openproject.config',
'openproject.uiComponents',
'openproject.timelines',
'openproject.workPackages',
'openproject.messages',
'openproject.timeEntries',
'ngAnimate',
'ngAria',
'ngSanitize',
'truncate',
'openproject.layout',
'cgBusy',
'openproject.api',
'openproject.templates',
'monospaced.elastic',
'openproject.inplace-edit',
'openproject.wpButtons',
'openproject.responsive'
]);
var opApp = require('./angular-modules.ts').default;
window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') ||
'';
window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') || '';
openprojectApp
opApp
.config([
'$locationProvider',
'$httpProvider',

@ -77,12 +77,20 @@ module.exports = function(I18n, $rootScope) {
_.remove(currentNotifications, function(element) {
return element === removedNotification;
});
},
clearNotifications = function() {
_.remove(currentNotifications);
};
$rootScope.$on('notification.remove', function(_e, notification) {
notificationRemoved(notification);
});
$rootScope.$on('notifications.clearAll', function() {
clearNotifications();
});
// public
var add = function(message) {
var notification = createNotification(message);

@ -203,7 +203,7 @@ declare namespace op {
public name:string;
public href:string;
constructor($source:restangular.IElement);
constructor($source: restangular.IElement, $loaded: boolean);
public $plain();
}

@ -31,7 +31,7 @@ module.exports = function() {
link: function(scope, element) {
var thead = element.parent('colgroup').siblings('thead');
thead.on('hover', 'th', function() {
thead.on('mouseenter mouseleave', 'th', function() {
if (element.index() === jQuery(this).index()) {
element.toggleClass('hover');
}

@ -55,14 +55,6 @@ angular.module('openproject.uiComponents')
.constant('FOCUSABLE_SELECTOR', 'a, button, :input, [tabindex], select')
.service('FocusHelper', ['$timeout', 'FOCUSABLE_SELECTOR', require(
'./focus-helper')])
.directive('hasDropdownMenu', [
'$rootScope',
'$injector',
'$window',
'$parse',
'FocusHelper',
require('./has-dropdown-menu-directive')
])
.directive('hasPreview', [
require('./has-preview-directive')
])
@ -104,7 +96,14 @@ angular.module('openproject.uiComponents')
.directive('zoomSlider', ['I18n', require('./zoom-slider-directive')])
.directive('clickNotification', ['$timeout','NotificationsService', require('./click-notification-directive')])
.directive('notifications', [require('./notifications-directive')])
.directive('notificationBox', ['I18n', '$timeout','$state','loadingIndicator', require('./notification-box-directive')])
.directive('notificationBox', [
'I18n',
'$timeout',
'$state',
'loadingIndicator',
'ConfigurationService',
require('./notification-box-directive')
])
.directive('uploadProgress', [require('./upload-progress-directive')])
.directive('attachmentIcon', [require('./attachment-icon-directive')])
.filter('ancestorsExpanded', require('./filters/ancestors-expanded-filter'))

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(I18n, $timeout,$state,loadingIndicator) {
module.exports = function(I18n, $timeout,$state,loadingIndicator,ConfigurationService) {
var notificationBoxController = function(scope, element) {
scope.uploadCount = 0;
@ -59,13 +59,15 @@ module.exports = function(I18n, $timeout,$state,loadingIndicator) {
}
};
$timeout(function() {
if (scope.content.type === 'error') {
element.focus();
} else {
element.find('.notification-box--close').focus();
}
});
if (ConfigurationService.accessibilityModeEnabled()) {
$timeout(function() {
if (scope.content.type === 'error') {
element.focus();
} else {
element.find('.notification-box--close').focus();
}
});
}
scope.$on('upload.error', function() {
if (scope.content.type === 'upload') {

@ -31,6 +31,12 @@ describe('NotificationBoxDirective', function() {
var $rootScope;
beforeEach(angular.mock.module('openproject.uiComponents', 'openproject.templates'));
beforeEach(angular.mock.module('openproject.services', function($provide) {
var configurationService = {};
configurationService.accessibilityModeEnabled = sinon.stub().returns(false);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;

@ -34,6 +34,13 @@ describe('NotificationsDirective', function() {
beforeEach(module('openproject.uiComponents'));
beforeEach(module('openproject.templates')); // see karmaConfig
beforeEach(angular.mock.module('openproject.services', function($provide) {
var configurationService = {};
configurationService.accessibilityModeEnabled = sinon.stub().returns(false);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;

@ -83,7 +83,11 @@ declare module restangular {
interface IService extends ICustom, IProvider {
one(route: string, id?: number): IElement;
one(route: string, id?: string): IElement;
getList(queryParams?: any, headers?: any): ICollectionPromise<any>;
getList<T>(queryParams?: any, headers?: any): ICollectionPromise<T>;
put(queryParams?: any, headers?: any): IPromise<any>;
oneUrl(route: string, url: string): IElement;
put(queryParams?: any, headers?: any): IPromise<any>;
all(route: string): IElement;
allUrl(route: string, url: string): IElement;
copy(fromElement: any): IElement;

@ -35,11 +35,13 @@ module API
path: :"#{property_name}",
namespace: path.to_s.pluralize,
getter: :"#{property_name}_id",
title_getter: -> (*) { model.send(property_name).name },
setter: :"#{getter}=")
@property_name = property_name
@path = path
@namespace = namespace
@getter = getter
@title_getter = title_getter
@setter = setter
super(model, current_user: nil)
@ -65,6 +67,18 @@ module API
represented.send(@setter, id)
},
render_nil: true
property :title,
exec_context: :decorator,
getter: -> (*) {
attribute = ::API::Utilities::PropertyNameConverter.to_ar_name(
@property_name,
context: represented
)
represented.try(attribute).try(:name)
},
writeable: false,
render_nil: false
end
end
end

Loading…
Cancel
Save