Merge pull request #1716 from opf/feature/refactor_angular_relations

[Feature] Refactor work package relations implementation
pull/1699/merge
manwithtwowatches 10 years ago
commit 26c3975608
  1. 11
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  2. 2
      app/assets/javascripts/angular/openproject-app.js
  3. 26
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  4. 39
      app/assets/javascripts/angular/work_packages/tabs/add-work-package-child-directive.js
  5. 37
      app/assets/javascripts/angular/work_packages/tabs/add-work-package-relation-directive.js
  6. 2
      app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js
  7. 84
      app/assets/javascripts/angular/work_packages/tabs/work-package-children-directive.js
  8. 51
      app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js
  9. 144
      app/assets/javascripts/angular/work_packages/view_models/relations-handler.js
  10. 2
      karma/tests/controllers/work-package-details-controller-test.js
  11. 121
      karma/tests/directives/work_packages/work-package-relations-directive-test.js
  12. 5
      public/templates/work_packages/tabs/_add_work_package_child.html
  13. 12
      public/templates/work_packages/tabs/_add_work_package_relation.html
  14. 48
      public/templates/work_packages/tabs/_work_package_children.html
  15. 42
      public/templates/work_packages/tabs/_work_package_parent.html
  16. 24
      public/templates/work_packages/tabs/_work_package_relations.html
  17. 46
      public/templates/work_packages/tabs/relations.html

@ -144,7 +144,7 @@ angular.module('openproject.workPackages.helpers')
for (var x = 0; x < children.length; x++) {
var child = children[x];
result.push(child.fetch());
result.push(child);
}
}
@ -168,15 +168,6 @@ angular.module('openproject.workPackages.helpers')
return result;
},
getRelatedWorkPackage: function(workPackage, relation) {
var self = workPackage.links.self.href;
if (relation.links.relatedTo.href == self) {
return relation.links.relatedFrom.fetch();
} else {
return relation.links.relatedTo.fetch();
}
},
//Note: The following methods are display helpers and so don't really belong here but are shared between
// directives so it's probably the best place for them just now.
getState: function(workPackage) {

@ -39,6 +39,7 @@ angular.module('openproject.models', [
'openproject.workPackages.config',
'openproject.services'
]);
angular.module('openproject.viewModels', ['openproject.services']);
// timelines
angular.module('openproject.timelines', [
@ -82,6 +83,7 @@ angular.module('openproject.workPackages.filters', [
angular.module('openproject.workPackages.config', []);
angular.module('openproject.workPackages.controllers', [
'openproject.models',
'openproject.viewModels',
'openproject.workPackages.helpers',
'openproject.services',
'openproject.workPackages.config',

@ -38,6 +38,15 @@ angular.module('openproject.workPackages.controllers')
precedes: "Relation::Precedes",
follows: "Relation::Follows"
})
.constant('RELATION_IDENTIFIERS', {
relatedTo: "relates",
duplicates: "duplicates",
duplicated: "duplicated",
blocks: "blocks",
blocked: "blocked",
precedes: "precedes",
follows: "follows"
})
.controller('WorkPackageDetailsController', [
'$scope',
@ -46,10 +55,14 @@ angular.module('openproject.workPackages.controllers')
'I18n',
'VISIBLE_LATEST',
'RELATION_TYPES',
'RELATION_IDENTIFIERS',
'$q',
'WorkPackagesHelper',
'ConfigurationService',
function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, $q, WorkPackagesHelper, ConfigurationService) {
'CommonRelationsHandler',
'ChildrenRelationsHandler',
'ParentRelationsHandler',
function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, RELATION_IDENTIFIERS, $q, WorkPackagesHelper, ConfigurationService, CommonRelationsHandler, ChildrenRelationsHandler, ParentRelationsHandler) {
$scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name);
});
@ -101,18 +114,23 @@ angular.module('openproject.workPackages.controllers')
// relations
$q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parents) {
$scope.wpParent = parents.length ? parents[0] : null;
var relationsHandler = new ParentRelationsHandler(workPackage, parents);
$scope.wpParent = relationsHandler;
});
$q.all(WorkPackagesHelper.getChildren(workPackage)).then(function(children) {
$scope.wpChildren = children;
var relationsHandler = new ChildrenRelationsHandler(workPackage, children);
$scope.wpChildren = relationsHandler;
});
for (var key in RELATION_TYPES) {
if (RELATION_TYPES.hasOwnProperty(key)) {
(function(key) {
$q.all(WorkPackagesHelper.getRelationsOfType(workPackage, RELATION_TYPES[key])).then(function(relations) {
$scope[key] = relations;
var relationsHandler = new CommonRelationsHandler(workPackage,
relations,
RELATION_IDENTIFIERS[key]);
$scope[key] = relationsHandler;
});
})(key);
}

@ -29,44 +29,9 @@
// TODO move to UI components
angular.module('openproject.workPackages.tabs')
.directive('workPackageParent', [
'I18n',
'PathHelper',
'WorkPackageService',
'WorkPackagesHelper',
'$timeout',
function(I18n, PathHelper, WorkPackageService, WorkPackagesHelper, $timeout) {
.directive('addWorkPackageChild', [function() {
return {
restrict: 'E',
replace: true,
scope: {
title: '@',
workPackage: '=',
parent: '=',
btnTitle: '@buttonTitle',
btnIcon: '@buttonIcon'
},
templateUrl: '/templates/work_packages/tabs/_work_package_parent.html',
link: function(scope, element, attrs) {
scope.I18n = I18n;
scope.getState = WorkPackagesHelper.getState;
scope.getFullIdentifier = WorkPackagesHelper.getFullIdentifier;
var setExpandState = function() {
scope.expand = !!scope.parent;
};
scope.$watch('parent', function() {
setExpandState();
});
scope.$watch('expand', function(newVal, oldVal) {
scope.stateClass = WorkPackagesHelper.collapseStateIcon(!newVal);
});
scope.toggleExpand = function() {
scope.expand = !scope.expand;
};
}
templateUrl: '/templates/work_packages/tabs/_add_work_package_child.html',
};
}]);

@ -0,0 +1,37 @@
//-- 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.
//++
// TODO move to UI components
angular.module('openproject.workPackages.tabs')
.directive('addWorkPackageRelation', [function() {
return {
restrict: 'E',
templateUrl: '/templates/work_packages/tabs/_add_work_package_relation.html',
};
}]);

@ -44,7 +44,7 @@ angular.module('openproject.workPackages.tabs')
scope.userPath = PathHelper.staticUserPath;
scope.canDeleteRelation = !!scope.relation.links.remove;
WorkPackagesHelper.getRelatedWorkPackage(scope.workPackage, scope.relation).then(function(relatedWorkPackage){
scope.handler.getRelatedWorkPackage(scope.workPackage, scope.relation).then(function(relatedWorkPackage){
scope.relatedWorkPackage = relatedWorkPackage;
scope.fullIdentifier = WorkPackagesHelper.getFullIdentifier(relatedWorkPackage);
scope.state = WorkPackagesHelper.getState(relatedWorkPackage);

@ -1,84 +0,0 @@
//-- 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.
//++
// TODO move to UI components
angular.module('openproject.workPackages.tabs')
.directive('workPackageChildren', [
'I18n',
'PathHelper',
'WorkPackageService',
'WorkPackagesHelper',
'$timeout',
function(I18n, PathHelper, WorkPackageService, WorkPackagesHelper, $timeout) {
return {
restrict: 'E',
replace: true,
scope: {
title: '@',
workPackage: '=',
children: '=',
btnTitle: '@buttonTitle',
btnIcon: '@buttonIcon'
},
templateUrl: '/templates/work_packages/tabs/_work_package_children.html',
link: function(scope, element, attrs) {
scope.I18n = I18n;
scope.userPath = PathHelper.staticUserPath;
scope.workPackagePath = PathHelper.staticWorkPackagePath;
scope.getState = WorkPackagesHelper.getState;
scope.getFullIdentifier = WorkPackagesHelper.getFullIdentifier;
var setExpandState = function() {
scope.expand = scope.children && scope.children.length > 0;
};
scope.$watch('children', function() {
setExpandState();
scope.childrenCount = scope.children.length || 0;
});
scope.$watch('expand', function(newVal, oldVal) {
scope.stateClass = WorkPackagesHelper.collapseStateIcon(!newVal);
});
scope.toggleExpand = function() {
scope.expand = !scope.expand;
};
scope.addChild = function() {
// Temporarily go to old create view with parent_id set to currently viewed work package
window.location = PathHelper.staticWorkPackageNewWithParentPath(scope.workPackage.props.projectId, scope.workPackage.props.id);
}
scope.deleteChild = function() {
//TODO: Requires API endpoint for update work package
}
}
};
}]);

@ -43,9 +43,7 @@ angular.module('openproject.workPackages.tabs')
replace: true,
scope: {
title: '@',
workPackage: '=',
relations: '=',
relationIdentifier: '=',
handler: '=',
btnTitle: '@buttonTitle',
btnIcon: '@buttonIcon',
isSingletonRelation: '@singletonRelation'
@ -53,16 +51,23 @@ angular.module('openproject.workPackages.tabs')
templateUrl: '/templates/work_packages/tabs/_work_package_relations.html',
link: function(scope, element, attrs) {
scope.I18n = I18n;
scope.canAddRelation = !!scope.workPackage.links.addRelation;
var setExpandState = function() {
scope.expand = scope.relations && scope.relations.length > 0;
scope.expand = !scope.handler.isEmpty();
};
scope.$watch('relations', function() {
scope.$watch('handler', function() {
if (scope.handler) {
scope.workPackage = scope.handler.workPackage;
setExpandState();
if(scope.relations) {
scope.relationsCount = scope.relations.length || 0;
scope.relationsCount = scope.handler.getCount();
if (scope.handler.applyCustomExtensions) {
$timeout(function() {
scope.handler.applyCustomExtensions();
});
}
}
});
@ -73,36 +78,6 @@ angular.module('openproject.workPackages.tabs')
scope.toggleExpand = function() {
scope.expand = !scope.expand;
};
scope.addRelation = function() {
var inputElement = angular.element('#relation_to_id-' + scope.relationIdentifier);
var toId = inputElement.val();
WorkPackageService.addWorkPackageRelation(scope.workPackage, toId, scope.relationIdentifier).then(function(relation) {
inputElement.val('');
scope.$emit('workPackageRefreshRequired', '');
}, function(error) {
ApiHelper.handleError(scope, error);
});
};
// Massive hack alert - Using old prototype autocomplete ///////////
if(scope.canAddRelation) {
$timeout(function(){
var url = PathHelper.workPackageAutoCompletePath(scope.workPackage.props.projectId, scope.workPackage.props.id);
new Ajax.Autocompleter('relation_to_id-' + scope.relationIdentifier,
'related_issue_candidates-' + scope.relationIdentifier,
url,
{ minChars: 1,
frequency: 0.5,
paramName: 'q',
updateElement: function(value) {
document.getElementById('relation_to_id-' + scope.relationIdentifier).value = value.id;
},
parameters: 'scope=all'
});
});
}
////////////////////////////////////////////////////////////////////
}
};
}]);

@ -0,0 +1,144 @@
//-- 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.viewModels')
.factory('CommonRelationsHandler', [
'$timeout',
'WorkPackageService',
'ApiHelper',
function($timeout, WorkPackageService, ApiHelper) {
function CommonRelationsHandler(workPackage,
relations,
relationsId) {
this.workPackage = workPackage;
this.relations = relations;
this.relationsId = relationsId;
this.type = "relation";
this.isSingletonRelation = false;
}
CommonRelationsHandler.prototype = {
isEmpty: function() {
return !this.relations || this.relations.length === 0;
},
getCount: function() {
return (this.relations) ? this.relations.length : 0;
},
canAddRelation: function() {
return !!this.workPackage.links.addRelation;
},
addRelation: function(scope) {
var inputElement = angular.element('#relation_to_id-' + this.relationsId);
var toId = inputElement.val();
WorkPackageService.addWorkPackageRelation(this.workPackage, toId, this.relationsId).then(function(relation) {
inputElement.val('');
scope.$emit('workPackageRefreshRequired', '');
}, function(error) {
ApiHelper.handleError(scope, error);
});
},
applyCustomExtensions: function() {
// Massive hack alert - Using old prototype autocomplete ///////////
if(this.canAddRelation) {
var workPackage = this.workPackage;
var relationsId = this.relationsId;
$timeout(function() {
var url = PathHelper.workPackageAutoCompletePath(workPackage.props.projectId, workPackage.props.id);
new Ajax.Autocompleter('relation_to_id-' + relationsId,
'related_issue_candidates-' + relationsId,
url,
{ minChars: 1,
frequency: 0.5,
paramName: 'q',
updateElement: function(value) {
document.getElementById('relation_to_id-' + relationsId).value = value.id;
},
parameters: 'scope=all'
});
});
}
////////////////////////////////////////////////////////////////////
},
getRelatedWorkPackage: function(workPackage, relation) {
var self = workPackage.links.self.href;
if (relation.links.relatedTo.href == self) {
return relation.links.relatedFrom.fetch();
} else {
return relation.links.relatedTo.fetch();
}
}
};
return CommonRelationsHandler;
}])
.factory('ChildrenRelationsHandler', ['PathHelper',
'CommonRelationsHandler',
function(PathHelper,
CommonRelationsHandler) {
function ChildrenRelationsHandler(workPackage, children) {
var handler = new CommonRelationsHandler(workPackage, children, undefined);
handler.type = "child";
handler.canAddRelation = function() { return true };
handler.addRelation = function() {
window.location = PathHelper.staticWorkPackageNewWithParentPath(this.workPackage.props.projectId, this.workPackage.props.id);
};
handler.applyCustomExtensions = undefined;
handler.getRelatedWorkPackage = function(workPackage, relation) { return relation.fetch() };
return handler;
}
return ChildrenRelationsHandler;
}])
.factory('ParentRelationsHandler', ['ChildrenRelationsHandler',
function(ChildrenRelationsHandler) {
function ParentRelationsHandler(workPackage, parents) {
var handler = new ChildrenRelationsHandler(workPackage, parents, undefined);
handler.type = "parent";
handler.canAddRelation = function() { return false };
handler.addRelation = undefined;
handler.isSingletonRelation = true;
return handler;
}
return ParentRelationsHandler;
}]);

@ -159,7 +159,7 @@ describe('WorkPackageDetailsController', function() {
});
it('Relation::Relates', function() {
expect(scope.relatedTo.length).to.eq(1);
expect(scope.relatedTo).to.be.ok;
});
});
});

@ -43,7 +43,11 @@ describe('Work Package Relations Directive', function() {
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(inject(function($rootScope, $compile, _I18n_, _PathHelper_, _WorkPackagesHelper_) {
beforeEach(inject(function($rootScope,
$compile,
_I18n_,
_PathHelper_,
_WorkPackagesHelper_) {
scope = $rootScope.$new();
compile = function(html) {
@ -54,6 +58,7 @@ describe('Work Package Relations Directive', function() {
I18n = _I18n_;
PathHelper = _PathHelper_;
WorkPackagesHelper = _WorkPackagesHelper_;
Ajax = {
Autocompleter: angular.noop
}
@ -69,14 +74,43 @@ describe('Work Package Relations Directive', function() {
I18n.t.restore();
});
var multiElementHtml = "<work-package-relations title='MyRelation' work-package='workPackage' relations='relations' button-title='Add Relation' button-icon='%MyIcon%'></work-package-relation>"
var singleElementHtml = "<work-package-relations title='MyRelation' work-package='workPackage' relations='relations' button-title='Add Relation' button-icon='%MyIcon%' singleton-relation='true'></work-package-relation>"
var html = "<work-package-relations title='MyRelation' handler='relations' button-title='Add Relation' button-icon='%MyIcon%'></work-package-relations>"
var workPackage1;
var workPackage2;
var workPackage3;
var relationsHandlerEmpty;
var relationsHandlerSingle;
var relationsHandlerMulti;
var createRelationsHandlerStub = function($timeout, count) {
var relationsHandler = new Object();
relationsHandler.workPackage = sinon.stub();
relationsHandler.relationsId = sinon.stub();
relationsHandler.isEmpty = sinon.stub();
relationsHandler.getCount = sinon.stub();
relationsHandler.canAddRelation = sinon.stub();
relationsHandler.addRelation = sinon.stub();
relationsHandler.applyCustomExtensions = sinon.stub();
relationsHandler.workPackage.returns(workPackage1);
relationsHandler.relationsId.returns('related');
relationsHandler.isEmpty.returns(count === 0);
relationsHandler.getCount.returns(count);
relationsHandler.type = "relation";
relationsHandler.getRelatedWorkPackage = function() {
return $timeout(function() {
return workPackage1;
}, 10);
};
return relationsHandler;
};
beforeEach(inject(function($q, $timeout) {
workPackage1 = {
props: {
@ -155,12 +189,14 @@ describe('Work Package Relations Directive', function() {
}
};
WorkPackagesHelper.getRelatedWorkPackage = function() {
return $timeout(function() {
return workPackage1;
}, 10);
};
relationsHandlerEmpty = createRelationsHandlerStub($timeout, 0);
relationsHandlerEmpty.relations = [];
relationsHandlerSingle = createRelationsHandlerStub($timeout, 1);
relationsHandlerSingle.relations = [relation1];
relationsHandlerMulti = createRelationsHandlerStub($timeout, 2);
relationsHandlerMulti.relations = [relation1, relation2];
}));
var shouldBehaveLikeRelationsDirective = function() {
@ -228,11 +264,10 @@ describe('Work Package Relations Directive', function() {
};
var shouldBehaveLikeSingleRelationDirective = function() {
it('should not have an elements count', function() {
it('should NOT have an elements count', function() {
var title = angular.element(element.find('h3'));
expect(title.text()).not.to.include('(');
expect(title.text()).not.to.include(')');
expect(title.text()).to.not.include('(' + scope.relations.getCount() + ')');
});
};
@ -240,13 +275,13 @@ describe('Work Package Relations Directive', function() {
it('should have an elements count', function() {
var title = angular.element(element.find('h3'));
expect(title.text()).to.include('(' + scope.relations.length + ')');
expect(title.text()).to.include('(' + scope.relations.getCount() + ')');
});
};
var shouldBehaveLikeHasAddRelationDialog = function() {
it('should have add relation button and id input', function() {
var addRelationDiv = angular.element(element.find('.workpackages .add-relation'));
var addRelationDiv = angular.element(element.find('.content .add-relation'));
expect(addRelationDiv.length).not.to.eq(0);
var button = addRelationDiv.find('button');
@ -264,46 +299,47 @@ describe('Work Package Relations Directive', function() {
};
describe('no element markup', function() {
describe('single element behavior', function() {
beforeEach(function() {
scope.workPackage = workPackage1;
compile(singleElementHtml);
scope.relations = relationsHandlerMulti;
scope.relations.canAddRelation.returns(true);
scope.relations.isEmpty.returns(true);
compile(html);
});
shouldBehaveLikeSingleRelationDirective();
shouldBehaveLikeMultiRelationDirective();
shouldBehaveLikeCollapsedRelationsDirective();
shouldBehaveLikeHasAddRelationDialog();
});
describe('multi element behavior', function() {
beforeEach(function() {
scope.workPackage = workPackage1;
scope.relations = [];
describe('single element markup', function() {
describe('header', function() {
beforeEach(inject(function($timeout) {
scope.relations = relationsHandlerSingle;
scope.relations.isSingletonRelation = true;
compile(multiElementHtml);
});
compile(html);
shouldBehaveLikeMultiRelationDirective();
$timeout.flush();
}));
shouldBehaveLikeCollapsedRelationsDirective();
});
shouldBehaveLikeSingleRelationDirective();
});
describe('single element markup', function() {
describe('readonly', function() {
beforeEach(inject(function($timeout) {
scope.workPackage = workPackage2;
scope.relations = [relation1];
scope.relations = relationsHandlerSingle;
compile(singleElementHtml);
compile(html);
$timeout.flush();
}));
shouldBehaveLikeRelationsDirective();
shouldBehaveLikeSingleRelationDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
@ -315,17 +351,17 @@ describe('Work Package Relations Directive', function() {
describe('can add and remove relations', function() {
beforeEach(inject(function($timeout) {
scope.workPackage = workPackage1;
scope.relations = [relation2];
scope.relations = relationsHandlerSingle;
scope.relations.relations = [relation2];
scope.relations.canAddRelation.returns(true);
compile(singleElementHtml);
compile(html);
$timeout.flush();
}));
shouldBehaveLikeRelationsDirective();
shouldBehaveLikeSingleRelationDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
@ -333,21 +369,20 @@ describe('Work Package Relations Directive', function() {
shouldBehaveLikeHasTableContent(1, false);
shouldBehaveLikeHasAddRelationDialog();
}));
});
describe('table row of closed work package', function() {
beforeEach(inject(function($timeout) {
scope.workPackage = workPackage1;
scope.relations = [relation2];
scope.relations = relationsHandlerSingle;
scope.relations.relations = [relation2];
WorkPackagesHelper.getRelatedWorkPackage = function() {
scope.relations.getRelatedWorkPackage = function() {
return $timeout(function() {
return workPackage3;
}, 10);
};
compile(singleElementHtml);
compile(html);
$timeout.flush();
}));

@ -0,0 +1,5 @@
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="handler.addRelation()">
</button>

@ -0,0 +1,12 @@
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="handler.addRelation(this)">
</button>
<input id="relation_to_id-{{ handler.relationsId }}"
name="relation[to_id][{{ handler.relationsId }}]"
size="10"
type="text"
autocomplete="off">
<div id="related_issue_candidates-{{ handler.relationsId }}"
class="autocomplete related-issue-candidates"></div>

@ -1,48 +0,0 @@
<div class="relation">
<h3>
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
<span ng-if="!isSingletonRelation">({{ childrenCount }})</span>
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="children">
<table>
<thead>
<tr>
<td>{{ I18n.t('js.work_packages.properties.subject') }}</td>
<td>{{ I18n.t('js.work_packages.properties.status') }}</td>
<td>{{ I18n.t('js.work_packages.properties.assignee') }}</td>
<td></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="workPackage in children">
<td>
<a title="{{ getFullIdentifier(workPackage) }}" class="work_package" ng-class="getState(workPackage)" href="{{ workPackagePath(workPackage.props.id) }}">
{{ getFullIdentifier(workPackage) }}
</a>
</td>
<td title="{{ workPackage.props.status }}">{{ workPackage.props.status }}</td>
<td>
<a title="{{ workPackage.embedded.assignee.props.name }}" href="{{ userPath(workPackage.embedded.assignee.props.id) }}">
{{ workPackage.embedded.assignee.props.name }}
</a>
</td>
<td><!--i title="delete relation" class="delete-item icon-delete" ng-click="removeChild()"></i--></td>
</tr>
</tbody>
</table>
</div>
<div ng-if="!children || children.length === 0">
No child work packages
</div>
</div>
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="addChild()">
</button>
</div>
</div>

@ -1,42 +0,0 @@
<div class="relation">
<h3>
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="parent">
<table>
<thead>
<tr>
<td>{{ I18n.t('js.work_packages.properties.subject') }}</td>
<td>{{ I18n.t('js.work_packages.properties.status') }}</td>
<td>{{ I18n.t('js.work_packages.properties.assignee') }}</td>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<a title="{{ getFullIdentifier(parent) }}" class="work_package" ng-class="getState(parent)" href="{{ workPackagePath(parent.props.id) }}">
{{ getFullIdentifier(parent) }}
</a>
</td>
<td title="{{ parent.props.status }}">{{ parent.props.status }}</td>
<td>
<a title="{{ parent.embedded.assignee.props.name }}" href="{{ userPath(parent.embedded.assignee.props.id) }}">
{{ parent.embedded.assignee.props.name }}
</a>
</td>
<td><!--i title="delete relation" class="delete-item icon-delete" ng-click="removeChild()"></i--></td>
</tr>
</tbody>
</table>
</div>
<div ng-if="!parent">
No parent work packages
</div>
</div>
</div>
</div>

@ -2,12 +2,12 @@
<h3>
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
<span ng-if="!isSingletonRelation">({{ relationsCount }})</span>
<span ng-if="!handler.isSingletonRelation">({{ handler.getCount() }})</span>
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="relations">
<div ng-if="handler.relations">
<table>
<thead>
<tr>
@ -19,7 +19,7 @@
</thead>
<tbody>
<tr related-work-package-table-row
ng-repeat="relation in relations">
ng-repeat="relation in handler.relations">
<td>
<a title="{{ fullIdentifier }}" class="work_package" ng-class="state" href="{{ workPackagePath(relatedWorkPackage.props.id) }}">
{{ fullIdentifier }}
@ -41,23 +41,13 @@
</tbody>
</table>
</div>
<div ng-if="!relations || relations.length === 0">
<div ng-if="handler.isEmpty()">
No relation exists
</div>
</div>
<div class="add-relation" ng-if="canAddRelation">
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="addRelation()">
</button>
<input id="relation_to_id-{{ relationIdentifier }}"
name="relation[to_id][{{ relationIdentifier }}]"
size="10"
type="text"
autocomplete="off">
<div id="related_issue_candidates-{{ relationIdentifier }}"
class="autocomplete related-issue-candidates"></div>
<div class="add-relation" ng-if="handler.canAddRelation()" ng-switch="handler.type">
<add-work-package-child ng-switch_when="child"></add-work-package-child>
<add-work-package-relation ng-switch_when="relation"></add-work-package-relation>
</div>
</div>
</div>

@ -1,73 +1,55 @@
<div class="detail-panel-description">
<div class="detail-panel-description-content">
<work-package-parent title="{{ I18n.t('js.relation_labels.parent') }}"
work-package="workPackage"
parent="wpParent"
relation-identifier="'parent'"
<work-package-relations title="{{ I18n.t('js.relation_labels.parent') }}"
handler="wpParent"
button-title="Change Parent"
button-icon="<i class='icon-hierarchy icon-edit'></i>"
singleton-relation="true">
</work-package-parent>
button-icon="<i class='icon-hierarchy icon-edit'></i>">
</work-package-relations>
<work-package-children title="{{ I18n.t('js.relation_labels.children') }}"
work-package="workPackage"
children="wpChildren"
<work-package-relations title="{{ I18n.t('js.relation_labels.children') }}"
handler="wpChildren"
button-title="Add Child"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-children>
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.relatedTo') }}"
work-package="workPackage"
relations="relatedTo"
relation-identifier="'relates'"
handler="relatedTo"
button-title="Add Related to"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.duplicates') }}"
work-package="workPackage"
relations="duplicates"
relation-identifier="'duplicates'"
handler="duplicates"
button-title="Add Duplicates"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.duplicated') }}"
work-package="workPackage"
relations="duplicated"
relation-identifier="'duplicated'"
handler="duplicated"
button-title="Add Duplicated by"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.blocks') }}"
work-package="workPackage"
relations="blocks"
relation-identifier="'blocks'"
handler="blocks"
button-title="Add Blocks"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.blocked') }}"
work-package="workPackage"
relations="blocked"
relation-identifier="'blocked'"
handler="blocked"
button-title="Add Blocked by"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.precedes') }}"
work-package="workPackage"
relations="precedes"
relation-identifier="'precedes'"
handler="precedes"
button-title="Add Precedes"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.follows') }}"
work-package="workPackage"
relations="follows"
relation-identifier="'follows'"
handler="follows"
button-title="Add Follows"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>

Loading…
Cancel
Save