added translations

pull/4748/head
manuschiller 8 years ago
parent 57b04a59fc
commit 7ecad10c9b
  1. 42
      app/assets/stylesheets/content/work_packages/tabs/_relations.sass
  2. 24
      config/locales/js-en.yml
  3. 6
      frontend/app/components/wp-panels/relations-panel/relations-panel.directive.html
  4. 22
      frontend/app/components/wp-panels/relations-panel/relations-panel.directive.ts
  5. 30
      frontend/app/components/wp-relations/add-wp-relation/add-wp-relation.directive.html
  6. 102
      frontend/app/components/wp-relations/wp-relation-group/wp-child-relation-group.service.ts
  7. 108
      frontend/app/components/wp-relations/wp-relation-group/wp-parent-relation-group.service.ts
  8. 136
      frontend/app/components/wp-relations/wp-relation-group/wp-relation-group.service.ts
  9. 104
      frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.directive.ts
  10. 30
      frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html
  11. 46
      frontend/app/components/wp-relations/wp-relations-create/add-child.template.html
  12. 21
      frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html
  13. 47
      frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html
  14. 34
      frontend/app/components/wp-relations/wp-relations-create/empty-parents.template.html
  15. 129
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts
  16. 21
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html
  17. 117
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.directive.ts
  18. 38
      frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts
  19. 16
      frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html
  20. 75
      frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts
  21. 41
      frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.template.html
  22. 130
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts
  23. 89
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service.ts
  24. 19
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html
  25. 93
      frontend/app/components/wp-relations/wp-relations.directive.html
  26. 344
      frontend/app/components/wp-relations/wp-relations.directive.test.ts
  27. 190
      frontend/app/components/wp-relations/wp-relations.directive.ts
  28. 28
      frontend/app/components/wp-relations/wp-relations.interfaces.ts
  29. 111
      frontend/app/components/wp-relations/wp-relations.service.ts
  30. 15
      frontend/app/components/wp-relations/wp-relations.template.html
  31. 60
      frontend/app/components/wp-relations/wp-single-relation.directive.ts

@ -44,3 +44,45 @@
top: auto
input[type='text']
width: 100%
.tab-content--padding-right
padding-right: 25px
.hierarchy-item
margin-bottom: 2px
.relation-row
line-height: 1.5em
.attribute-header
font-size: 0.8em
text-transform: uppercase
font-weight: bold
.description-section
border: 1px dotted lightblue
padding: 4px
.relation-create
font-size: 0.8em
.wp-relations-hierarchy-section
margin-top: 35px
.wp-relations-controls-section
text-align: right
.force-right
position: absolute
right: 0
.wp-relations-create-button
.-create-button-full-width
margin-top: 1.5em
width: 100%
padding-right: 25px
.wp-relations-group
/**margin-top: 1.5em**/
.wp-relations-status-field
margin-left: 2px

@ -271,16 +271,23 @@ en:
precedes: "Precedes"
follows: "Follows"
relations_hierarchy:
hierarchy_headline: "hierarchy"
relation_buttons:
change_parent: "Change parent"
add_child: "Add child"
add_related_to: "Add related to"
add_duplicates: "Add duplicates"
add_duplicated_by: "Add duplicated by"
add_blocks: "Add blocks"
add_blocked_by: "Add blocked by"
add_precedes: "Add precedes"
add_follows: "Add follows"
remove_parent: "Remove parent"
add_parent: "Add existing parent work package"
add_new_child: "Create new child work package"
add_existing_child: "Add existing child work package"
remove_child: "Remove child work package"
add_new_relation: "Create new relation"
remove: "Remove relation"
save: "Save relation"
abort: "Abort"
relations_autocomplete:
placeholder: "Enter the related work package id"
repositories:
select_tag: 'Select tag'
@ -404,7 +411,6 @@ en:
message_error_during_bulk_delete: An error occurred while trying to delete work packages.
message_successful_bulk_delete: Successfully deleted work packages.
message_successful_show_in_fullscreen: "Click here to open this work package in fullscreen view."
message_view_spent_time: "View logged time"
no_value: "No value"
inline_create:
title: 'Click here to add a new work package to this list'

@ -1,10 +1,6 @@
<div class="detail-panel-description" ng-if="$ctrl.workPackage">
<div class="detail-panel-description-content">
<wp-relations
ng-repeat="relationGroup in $ctrl.relationGroups"
work-package="$ctrl.workPackage"
relation-group="relationGroup"
button-title="$ctrl.relationTitles[relationGroup.name]"
<wp-relations work-package="$ctrl.workPackage"
></wp-relations>
</div>
</div>

@ -32,28 +32,6 @@ import {WorkPackageRelationsService} from "../../wp-relations/wp-relations.servi
export class RelationsPanelController {
public workPackage:WorkPackageResourceInterface;
public relationTitles;
public relationGroups;
constructor(I18n: op.I18n,
wpRelations:WorkPackageRelationsService) {
this.relationTitles = {
parent: I18n.t('js.relation_buttons.change_parent'),
children: I18n.t('js.relation_buttons.add_child'),
relatedTo: I18n.t('js.relation_buttons.add_related_to'),
duplicates: I18n.t('js.relation_buttons.add_duplicates'),
duplicated: I18n.t('js.relation_buttons.add_duplicated_by'),
blocks: I18n.t('js.relation_buttons.add_blocks'),
blocked: I18n.t('js.relation_buttons.add_blocked_by'),
precedes: I18n.t('js.relation_buttons.add_precedes'),
follows: I18n.t('js.relation_buttons.add_follows')
};
this.workPackage.relations.$load().then(() => {
this.relationGroups = wpRelations.getWpRelationGroups(this.workPackage);
});
}
}
function relationsPanelDirective() {

@ -1,30 +0,0 @@
<form name="add_relation_form" class="form">
<div class="choice">
<div class="choice--select">
<ui-select
id="relation_to_id-{{ $ctrl.relationGroup.id }}"
name="relation[to_id][{{ $ctrl.relationGroup.id }}]"
ng-model="$ctrl.wpToAddId"
title="{{ text.uiSelectTitle }}"
required
theme="select2"
tabindex="0">
<ui-select-match tabindex="-1">{{$select.selected.to_s}}</ui-select-match>
<ui-select-choices
refresh-delay="250"
refresh="autocompleteWorkPackages($select.search)"
repeat="item.id as item in options">
<div ng-bind="item.to_s"></div>
</ui-select-choices>
</ui-select>
</div>
<div class="choice--button">
<button class="button"
title="{{ $ctrl.btnTitle }}"
ng-bind-html="$ctrl.btnIcon + ' ' + $ctrl.btnTitle"
ng-click="$ctrl.addRelation()"
ng-disabled="add_relation_form.$invalid">
</button>
</div>
</div>
</form>

@ -1,102 +0,0 @@
// -- 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 {WorkPackageRelationGroup} from './wp-relation-group.service';
import {wpTabsModule} from '../../../angular-modules';
var $state:ng.ui.IStateService;
var $q:ng.IQService;
export class WorkPackageChildRelationsGroup extends WorkPackageRelationGroup {
public get canAddRelation() {
return !!this.workPackage.addChild;
}
public canRemoveRelation() {
return !!this.workPackage.update;
}
public getRelatedWorkPackage(relation) {
return relation.$load();
}
public addWpRelation():ng.IPromise<any> {
return this.workPackage.project.$load()
.then(() => {
const args = [
'work-packages.list.new',
{
parent_id: this.workPackage.id,
projectPath: this.workPackage.project.identifier
}
];
if ($state.includes('work-packages.show')) {
args[0] = 'work-packages.new';
}
(<any>$state).go(...args);
});
}
public removeWpRelation(relation) {
const deferred = $q.defer();
const index = this.relations.indexOf(relation);
relation.$load()
.then(workPackage => {
workPackage.parentId = null;
workPackage.save()
.then(() => {
this.relations.splice(index, 1);
deferred.resolve(index);
})
.catch(deferred.reject);
})
.catch(deferred.reject);
return deferred.promise;
}
protected init() {
if (Array.isArray(this.workPackage.children)) {
this.relations.push(...this.workPackage.children);
}
}
}
function wpChildRelationsGroupService(...args) {
[$state, $q] = args;
return WorkPackageChildRelationsGroup;
}
wpChildRelationsGroupService.$inject = ['$state', '$q'];
wpTabsModule.factory('WorkPackageChildRelationsGroup', wpChildRelationsGroupService);

@ -1,108 +0,0 @@
//-- 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 {WorkPackageRelationGroup} from './wp-relation-group.service';
import {wpTabsModule} from '../../../angular-modules';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {ErrorResource} from '../../api/api-v3/hal-resources/error-resource.service';
var $q:ng.IQService;
var HalResource;
var PathHelper:any;
var wpCacheService:WorkPackageCacheService;
var wpNotificationsService:WorkPackageNotificationService;
export class WorkPackageParentRelationGroup extends WorkPackageRelationGroup {
public get canAddRelation():boolean {
return !!this.workPackage.changeParent;
}
public canRemoveRelation():boolean {
return this.canAddRelation;
}
public getRelatedWorkPackage(relation) {
return relation.$load();
}
public addWpRelation(wpId:number) {
return this.changeParent(wpId).then(() => {
if (this.workPackage.parent) {
this.workPackage.parent.$load().then(parent => {
this.relations[0] = parent;
});
}
});
}
public removeWpRelation() {
return this.changeParent(null).then(() => {
this.relations.pop();
return 0;
});
}
protected changeParent(parentId:number) {
var params = {
parentId: parentId,
lockVersion: this.workPackage.lockVersion
};
return this.workPackage.changeParent(params)
.then((wp) => {
this.workPackage = wp;
return wpCacheService.updateWorkPackage(wp);
})
.catch(error => {
if (error instanceof ErrorResource) {
wpNotificationsService.showError(error, this.workPackage);
}
else {
wpNotificationsService.showGeneralError();
}
});
}
protected init() {
if (this.workPackage.parent) {
this.workPackage.parent.$load().then(parent => this.relations.push(parent));
}
}
}
function wpParentRelationGroupService(...args) {
[$q, HalResource, PathHelper, wpCacheService, wpNotificationsService] = args;
return WorkPackageParentRelationGroup;
}
wpParentRelationGroupService.$inject = [
'$q', 'HalResource', 'PathHelper', 'wpCacheService', 'wpNotificationsService'
];
wpTabsModule.factory('WorkPackageParentRelationGroup', wpParentRelationGroupService);

@ -1,136 +0,0 @@
//-- 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 {wpTabsModule} from '../../../angular-modules';
import {WorkPackageRelationsConfigInterface} from '../wp-relations.service';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
declare var URI:any;
var $q:ng.IQService;
var $http:ng.IHttpService;
var PathHelper:any;
export class WorkPackageRelationGroup {
public relations = [];
public get name():string {
return this.config.name;
}
public get type():string {
return this.config.type;
}
public get id():string {
return this.config.id || this.name;
}
public get isEmpty():boolean {
return !this.relations.length;
}
public get canAddRelation() {
return !!this.workPackage.addRelation;
}
constructor(protected workPackage:WorkPackageResourceInterface,
protected config:WorkPackageRelationsConfigInterface) {
this.init();
}
public canRemoveRelation(relation):boolean {
return !!relation.remove;
}
public getRelatedWorkPackage(relation) {
if (relation.relatedTo.href === this.workPackage.href) {
return relation.relatedFrom.$load();
}
return relation.relatedTo.$load();
}
public findRelatableWorkPackages(search:string) {
const deferred = $q.defer();
var params;
this.workPackage.project.$load().then(() => {
params = {
q: search,
scope: 'relatable',
escape: false,
id: this.workPackage.id,
project_id: this.workPackage.project.id
};
$http({
method: 'GET',
url: URI(PathHelper.workPackageJsonAutoCompletePath()).search(params).toString()
})
.then((response:any) => deferred.resolve(response.data))
.catch(deferred.reject);
})
.catch(deferred.reject);
return deferred.promise;
}
public addWpRelation(wpId:number):ng.IPromise<any> {
return this.workPackage.addRelation({
to_id: wpId,
relation_type: this.id
})
.then(relation => this.relations.push(relation));
}
public removeWpRelation(relation) {
const index = this.relations.indexOf(relation);
return relation.remove().then(() => {
this.relations.splice(index, 1);
return index;
});
}
protected init() {
const elements = this.workPackage.relations.elements;
if (Array.isArray(elements)) {
this.relations.push(
...elements.filter(relation => relation._type === this.type));
}
}
}
function wpRelationGroupService(...args) {
[$q, $http, PathHelper] = args;
return WorkPackageRelationGroup;
}
wpRelationGroupService.$inject = ['$q', '$http', 'PathHelper'];
wpTabsModule.factory('WorkPackageRelationGroup', wpRelationGroupService);

@ -1,61 +1,63 @@
//-- 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 {wpTabsModule} from "../../../angular-modules";
import {WorkPackageRelationsController} from "../wp-relations.directive";
function wpRelationRowDirective(PathHelper) {
var getFullIdentifier = (workPackage) => {
var type = ' ';
if (workPackage.type) {
type += workPackage.type.name + ': ';
}
import {wpDirectivesModule} from '../../../angular-modules';
import {
WorkPackageResource,
WorkPackageResourceInterface
} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {RelatedWorkPackage, RelationResource} from '../wp-relations.interfaces';
class WpRelationRowDirectiveController {
public relatedWorkPackage:RelatedWorkPackage;
public relationType:string;
return `#${workPackage.id}${type}${workPackage.subject}`;
public showRelationInfo:boolean = false;
public userInputs = {
description: this.relatedWorkPackage.relatedBy.description,
showDescriptionEditForm: false
};
function wpRelationsDirectiveLink(scope) {
scope.workPackagePath = PathHelper.workPackagePath;
scope.userPath = PathHelper.userPath;
public relation:RelationResource = this.relatedWorkPackage.relatedBy;
constructor(public I18n,
protected $scope:ng.IScope,
protected wpCacheService,
protected PathHelper,
protected wpNotificationsService,
protected WpRelationsService) {
if (this.relation) {
var relationType = this.WpRelationsService.getRelationTypeObjectByType(this.relation._type);
this.relationType = angular.isDefined(relationType) ? this.WpRelationsService.getTranslatedRelationTitle(relationType.name) : 'unknown';
}
};
public toggleUserDescriptionForm() {
this.userInputs.showDescriptionEditForm = !this.userInputs.showDescriptionEditForm;
}
scope.$ctrl.relationGroup.getRelatedWorkPackage(scope.relation)
.then(relatedWorkPackage => {
scope.relatedWorkPackage = relatedWorkPackage;
scope.fullIdentifier = getFullIdentifier(relatedWorkPackage);
scope.state = relatedWorkPackage.status.isClosed ? 'closed' : '';
});
public removeRelation() {
this.WpRelationsService.removeCommonRelation(this.relation)
.then(() => {
this.$scope.$emit('wp-relations.removed', this.relation);
this.wpCacheService.updateWorkPackage([this.relatedWorkPackage]);
this.wpNotificationsService.showSave(this.relatedWorkPackage);
})
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.relatedWorkPackage));
}
}
function WpRelationRowDirective() {
return {
restrict: 'A',
link: wpRelationsDirectiveLink
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relation-row/wp-relation-row.template.html',
replace: true,
scope: {
relatedWorkPackage: '='
},
controller: WpRelationRowDirectiveController,
controllerAs: '$ctrl',
bindToController: true
};
}
wpTabsModule.directive('wpRelationRow', wpRelationRowDirective);
wpDirectivesModule.directive('wpRelationRow', WpRelationRowDirective);

@ -0,0 +1,30 @@
<div class="relation-row" ng-mouseover="$ctrl.showRelationControls = true" ng-mouseleave="$ctrl.showRelationControls = false">
<div class="grid-block hierarchy-item">
<div class="grid-content medium-3 collapse">
<span>{{$ctrl.relationType}}</span>
</div>
<div class="grid-content medium-4 collapse" wp-single-relation ng-if="$ctrl.relatedWorkPackage">
<a href="{{$singleRelation.workPackagePath($ctrl.relatedWorkPackage.id)}}" title="{{$singleRelation.getFullIdentifier($ctrl.relatedWorkPackage, true)}}">
{{$singleRelation.getFullIdentifier($ctrl.relatedWorkPackage, true)}}
</a>
</div>
<div class="grid-content medium-3 collapse wp-relations-status-field">
<div wp-edit-form="$ctrl.relatedWorkPackage" ng-if="$ctrl.relatedWorkPackage">
<div wp-edit-field="'status'"></div>
</div>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section">
<accessible-by-keyboard ng-show="$ctrl.showRelationControls"
ng-if="$ctrl.relation.remove"
execute="$ctrl.removeRelation($ctrl.relation)">
<icon-wrapper icon-name="remove"
icon-title="{{::$ctrl.I18n.t('js.relation_buttons.remove')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -0,0 +1,46 @@
<div>
<div class="wp-inline-create-button wp-relations-create-button -full-width" ng-if="!$relationsCreateCtrl.showRelationsCreateForm">
<div class="grid-block">
<div class="grid-content collapse">
<a class="wp-inline-create--add-link relation-create" ng-click="$relationsCreateCtrl.createNewChildWorkPackage()">
<i class="icon icon-add"></i>
<span>{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.add_new_child')}}</span>
</a>
</div>
<div class="grid-content collapse">
<a class="wp-inline-create--add-link relation-create" ng-click="$relationsCreateCtrl.toggleRelationsCreateForm()">
<i class="icon icon-add"></i>
<span>{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.add_existing_child')}}</span>
</a>
</div>
</div>
</div>
<div class="grid-block v-align" ng-if="$relationsCreateCtrl.showRelationsCreateForm">
<div class="grid-content medium-10">
<wp-relations-autocomplete
work-package="$relationsCreateCtrl.workPackage"
selected-wp-id="$relationsCreateCtrl.selectedWpId"
selected-relation-type="$relationsCreateCtrl.selectedRelationType"></wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$relationsCreateCtrl.createRelation()">
<icon-wrapper icon-name="checkmark"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.save')}}">
</icon-wrapper>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$relationsCreateCtrl.toggleRelationsCreateForm()">
<icon-wrapper icon-name="remove"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.abort')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -0,0 +1,21 @@
<div class="grid-block v-align collapse" ng-if="$relationsCreateCtrl.showRelationsCreateForm || $relationsCreateCtrl.externalFormToggle">
<div class="grid-content medium-10 collapse">
<wp-relations-autocomplete
selected-wp-id="$relationsCreateCtrl.selectedWpId"
selected-relation-type="$relationsCreateCtrl.selectedRelationType"></wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$relationsCreateCtrl.createRelation()">
<icon-wrapper icon-name="checkmark"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.save')}}">
</icon-wrapper>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$relationsCreateCtrl.toggleRelationsCreateForm()">
<icon-wrapper icon-name="remove"
icon-title="{{::$relationsCreateCtrl.I18n('js.relation_buttons.abort')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>

@ -0,0 +1,47 @@
<div>
<div class="wp-inline-create-button wp-relations-create-button"
ng-if="!$relationsCreateCtrl.showRelationsCreateForm">
<div class="grid-block">
<div class="grid-content collapse">
<a class="wp-inline-create--add-link relation-create"
ng-click="$relationsCreateCtrl.toggleRelationsCreateForm()">
<i class="icon icon-add"></i>
<span>{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.add_new_relation')}}</span>
</a>
</div>
</div>
</div>
<div class="grid-block v-align" ng-if="$relationsCreateCtrl.showRelationsCreateForm">
<div class="grid-content collapse medium-3">
<select
class="relationTypeSelect"
ng-model="$relationsCreateCtrl.selectedRelationType"
ng-options="relationType.label for relationType in $relationsCreateCtrl.relationTypes">
</select>
</div>
<div class="grid-content medium-7">
<wp-relations-autocomplete
work-package="$relationsCreateCtrl.workPackage"
selected-wp-id="$relationsCreateCtrl.selectedWpId"
selected-relation-type="$relationsCreateCtrl.selectedRelationType"></wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$relationsCreateCtrl.createRelation()">
<icon-wrapper icon-name="checkmark"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.save')}}">
</icon-wrapper>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$relationsCreateCtrl.toggleRelationsCreateForm()">
<icon-wrapper icon-name="remove"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.abort')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -0,0 +1,34 @@
<div>
<div class="wp-inline-create-button wp-relations-create-button -full-width" ng-if="!$relationsCreateCtrl.showRelationsCreateForm" style="width:100%;padding-right:25px;">
<div class="grid-block">
<div class="grid-content collapse">
<a class="wp-inline-create--add-link relation-create" ng-click="$relationsCreateCtrl.toggleRelationsCreateForm()">
<i class="icon icon-add"></i>
<span>{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.add_parent')}}</span>
</a>
</div>
</div>
</div>
<div class="grid-block v-align" ng-if="$relationsCreateCtrl.showRelationsCreateForm || $relationsCreateCtrl.externalFormToggle">
<div class="grid-content medium-10 collapse">
<wp-relations-autocomplete
work-package="$relationsCreateCtrl.workPackage"
selected-wp-id="$relationsCreateCtrl.selectedWpId"
selected-relation-type="$relationsCreateCtrl.selectedRelationType"></wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$relationsCreateCtrl.createRelation()">
<icon-wrapper icon-name="checkmark"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.save')}}">
</icon-wrapper>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$relationsCreateCtrl.toggleRelationsCreateForm()">
<icon-wrapper icon-name="remove"
icon-title="{{::$relationsCreateCtrl.I18n.t('js.relation_buttons.abort')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -0,0 +1,129 @@
//-- 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 {wpDirectivesModule} from '../../../../angular-modules';
import {WorkPackageRelationsController} from "../../wp-relations.directive";
import {WorkPackageRelationsHierarchyController} from "../../wp-relations-hierarchy/wp-relations-hierarchy.directive";
import {WorkPackageResourceInterface} from "../../../api/api-v3/hal-resources/work-package-resource.service";
function wpRelationsAutocompleteDirective($q, PathHelper, $http) {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html',
require: ['^wpRelations', '?^wpRelationsHierarchy'],
scope: {
selectedWpId: '=',
selectedRelationType: '=',
workPackage: '='
},
link: function (scope, element, attrs, controllers ) {
scope.relatedWps = [];
getRelatedWorkPackages();
scope.onSelect = function(wpId){
scope.selectedWpId = wpId;
};
scope.autocompleteWorkPackages = (term) => {
if (!term) {
return;
}
findRelatableWorkPackages(term).then((workPackages:Array<WorkPackageResourceInterface>) => {
// reject already related work packages, self, children and parent
// to prevent invalid relations
scope.options = _.reject(workPackages, (wp) => {
return scope.relatedWps.indexOf(parseInt((wp.id as string))) > -1;
});
});
};
function findRelatableWorkPackages(search:string) {
const deferred = $q.defer();
var params;
scope.workPackage.project.$load().then(() => {
params = {
q: search,
scope: 'relatable',
escape: false,
id: scope.workPackage.id,
project_id: scope.workPackage.project.id
};
$http({
method: 'GET',
url: URI(PathHelper.workPackageJsonAutoCompletePath()).search(params).toString()
})
.then((response:any) => {
// THE JSON AUTOCOMPLETE SHOULD BE EXTENDED TO CONTAIN A REFERENCE TO THE
// ACTUAL WP-TYPE SINCE MILESTONES MAY NOT BE A PARENT ELEMENT AND THEREFORE THEY
// MUST BE REJECTED
deferred.resolve(response.data);
})
.catch(deferred.reject);
})
.catch(deferred.reject);
return deferred.promise;
}
function getRelatedWorkPackages() {
/** NOTE: THIS METHOD COULD PROBABLY DONE MORE EFFICIENTLY BY THE BACKEND **/
const wpRelationsController:WorkPackageRelationsController = controllers[0];
const wpRelationsHierarchyController:WorkPackageRelationsHierarchyController = controllers[1];
let wps = [scope.workPackage.id];
wps = wps.concat(wpRelationsController.currentRelations.map(relation => relation.id));
if (scope.workPackage.parentId) {
wps.push(scope.workPackage.parentId);
}
if (wpRelationsHierarchyController && wpRelationsHierarchyController.children) {
wps = wps.concat(wpRelationsHierarchyController.children.map(child => child.id));
} else {
if (scope.workPackage.children) {
var childPromises = [];
if (scope.workPackage.children.length > 0) {
childPromises = childPromises.concat(scope.workPackage.children.map(child => child.$load()));
$q.all(childPromises).then(children => {
wps = wps.concat(children.map(child => child.id));
scope.relatedWps = wps;
});
}
}
}
scope.relatedWps = wps;
}
}
};
}
wpDirectivesModule.directive('wpRelationsAutocomplete', wpRelationsAutocompleteDirective);

@ -0,0 +1,21 @@
<form name="add_relation_form" class="form">
<div class="dropdown-wrapper">
<ui-select
class="inplace-edit--select -full-width"
ng-model="selectedWpId"
on-select="onSelect($model)"
append-to-body="true"
required
theme="select2"
tabindex="0">
<ui-select-match tabindex="-1">{{$select.selected.to_s}}</ui-select-match>
<ui-select-choices
refresh-delay="250"
refresh="autocompleteWorkPackages($select.search)"
repeat="item.id as item in options | filter: item.config.canAdd">
<div ng-bind="item.to_s"></div>
</ui-select-choices>
</ui-select>
</div>
</form>

@ -0,0 +1,117 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {RelationType} from "../wp-relations.interfaces";
export class WpRelationsCreateController {
public showRelationsCreateForm: boolean = false;
public workPackage:WorkPackageResourceInterface;
public selectedRelationType:RelationType;
public selectedWpId:string;
public externalFormToggle: boolean;
public fixedRelationType:string;
public relationTypes = this.WpRelationsService.getRelationTypes(true);
public translatedRelationTitle = this.WpRelationsService.getTranslatedRelationTitle;
protected relationTitles = this.WpRelationsService.configuration.relationTitles;
constructor(public I18n,
protected $scope,
protected $rootScope,
protected $state,
protected WpRelationsService,
protected WpRelationsHierarchyService,
protected wpNotificationsService,
protected wpCacheService) {
var defaultRelationType = angular.isDefined(this.fixedRelationType) ? this.fixedRelationType : 'relatedTo';
this.selectedRelationType = this.WpRelationsService.getRelationTypeObjectByName(defaultRelationType);
if (angular.isDefined(this.externalFormToggle)) {
this.showRelationsCreateForm = this.externalFormToggle;
}
}
public createRelation() {
if (!this.selectedRelationType || ! this.selectedWpId) {
return;
}
switch (this.selectedRelationType.name) {
case 'parent':
this.changeParent();
break;
case 'children':
this.addExistingChildRelation();
break;
default:
this.createCommonRelation();
break;
}
}
protected addExistingChildRelation() {
this.WpRelationsHierarchyService.addExistingChildWp(this.workPackage, this.selectedWpId)
.then(newChildWp => this.$scope.$emit('wp-relations.addedChild', newChildWp))
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.workPackage))
.finally(this.toggleRelationsCreateForm());
}
protected createNewChildWorkPackage() {
this.WpRelationsHierarchyService.addNewChildWp(this.workPackage);
}
protected changeParent() {
this.WpRelationsHierarchyService.changeParent(this.workPackage, this.selectedWpId)
.then(updatedWp => {
this.$rootScope.$broadcast('wp-relations.changedParent', {
updatedWp: updatedWp,
parentId: this.selectedWpId
});
this.wpNotificationsService.showSave(this.workPackage);
})
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.workPackage))
.finally(this.toggleRelationsCreateForm());
}
protected createCommonRelation() {
let relationType = this.selectedRelationType.name === 'relatedTo' ? this.selectedRelationType.id : this.selectedRelationType.name;
this.WpRelationsService.addCommonRelation(this.workPackage, relationType, this.selectedWpId)
.then(relation => {
this.$scope.$emit('wp-relations.added', relation);
this.wpNotificationsService.showSave(this.workPackage);
})
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.workPackage))
.finally(() => this.toggleRelationsCreateForm());
}
public toggleRelationsCreateForm() {
this.showRelationsCreateForm = !this.showRelationsCreateForm;
this.externalFormToggle = !this.externalFormToggle;
}
}
function wpRelationsCreate() {
return {
restrict: 'E',
replace: true,
templateUrl: (el, attrs) => {
return '/components/wp-relations/wp-relations-create/' + attrs.template + '.template.html';
},
scope: {
workPackage: '=?',
fixedRelationType: '@?',
externalFormToggle: '=?'
},
controller: WpRelationsCreateController,
bindToController: true,
controllerAs: '$relationsCreateCtrl',
};
}
wpDirectivesModule.directive('wpRelationsCreate', wpRelationsCreate);

@ -26,27 +26,33 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {wpTabsModule} from "../../../angular-modules";
import {WorkPackageRelationsController} from "../wp-relations.directive";
import {wpDirectivesModule} from '../../../angular-modules';
import {RelatedWorkPackage} from '../wp-relations.interfaces';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
function addWpRelationDirective(I18n) {
export class WorkPackageRelationsGroupController {
public relatedWorkPackages:Array<RelatedWorkPackage>;
public workPackage:WorkPackageResourceInterface;
public wpType:string;
}
function wpRelationsGroupDirective() {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/add-wp-relation/add-wp-relation.directive.html',
replace: true,
templateUrl: '/components/wp-relations/wp-relations-group/wp-relations-group.template.html',
link: function (scope) {
scope.text = {
uiSelectTitle: I18n.t('js.field_value_enter_prompt', {field: scope.$ctrl.text.title})
};
scope.autocompleteWorkPackages = (term) => {
if (!term) return;
scope: {
wpType: '=',
workPackage: '=',
relatedWorkPackages: '='
},
scope.$ctrl.relationGroup.findRelatableWorkPackages(term).then(workPackages => {
scope.options = workPackages;
});
};
}
controller: WorkPackageRelationsGroupController,
controllerAs: '$ctrl',
bindToController: true,
};
}
wpTabsModule.directive('addWpRelation', addWpRelationDirective);
wpDirectivesModule.directive('wpRelationsGroup', wpRelationsGroupDirective);

@ -0,0 +1,16 @@
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
{{$ctrl.wpType}}
</h3>
</div>
</div>
<div class="content" ng-if="$ctrl.relatedWorkPackages">
<wp-relation-row
related-work-package="relatedWorkPackage"
ng-repeat="relatedWorkPackage in $ctrl.relatedWorkPackages"></wp-relation-row>
</div>
</div>

@ -0,0 +1,75 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {
WorkPackageResource,
WorkPackageResourceInterface
} from "../../api/api-v3/hal-resources/work-package-resource.service";
class WpRelationsHierarchyRowDirectiveController {
public workPackage;
public relatedWorkPackage;
public relationType;
public showEditForm: boolean = false;
public workPackagePath = this.PathHelper.workPackagePath;
constructor(public I18n,
protected $scope,
protected WpRelationsHierarchyService,
protected wpNotificationsService,
protected wpCacheService,
protected PathHelper,
protected wpNotificationsService) {
if (!this.relatedWorkPackage && this.relationType !== 'parent') {
this.relatedWorkPackage = angular.copy(this.workPackage);
}
};
public removeRelation() {
if (this.relationType === 'child') {
this.removeChild();
}else if (this.relationType === 'parent') {
this.removeParent();
}
}
protected removeChild() {
this.WpRelationsHierarchyService.removeChild(this.relatedWorkPackage).then(exChildWp => {
this.$scope.$emit('wp-relations.removedChild', exChildWp);
this.wpNotificationsService.showSave(this.workPackage);
})
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.relatedWorkPackage));;
}
protected removeParent() {
this.WpRelationsHierarchyService.removeParent(this.workPackage)
.then((updatedWp) => {
this.$scope.$emit('wp-relations.changedParent', {
updatedWp: this.workPackage,
parentId: null
});
this.wpNotificationsService.showSave(this.workPackage);
})
.catch(err => this.wpNotificationsService.handleErrorResponse(err, this.relatedWorkPackage));;
}
}
function WpRelationsHierarchyRowDirective() {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.template.html',
replace: true,
scope: {
indentBy: '@?',
workPackage: '=',
relatedWorkPackage: '=?',
relationType: '@'
},
controller: WpRelationsHierarchyRowDirectiveController,
controllerAs: '$ctrl',
bindToController: true
};
}
wpDirectivesModule.directive('wpRelationsHierarchyRow', WpRelationsHierarchyRowDirective);

@ -0,0 +1,41 @@
<div class="relation-row" ng-mouseover="$ctrl.showRelationControls = true" ng-mouseleave="$ctrl.showRelationControls = false">
<div class="grid-block v-align hierarchy-item" ng-if="!$ctrl.showEditForm && $ctrl.relatedType !== 'parent' && $ctrl.relatedWorkPackage">
<div class="grid-content medium-6 collapse" wp-single-relation>
<span ng-style="{'padding-left': $ctrl.indentBy + 'px'}">
<a href="{{$singleRelation.workPackagePath($ctrl.relatedWorkPackage.id)}}">
{{$singleRelation.getFullIdentifier($ctrl.relatedWorkPackage)}}
</a>
</span>
</div>
<div class="grid-content medium-4 collapse wp-relations-status-field">
<div wp-edit-form="$ctrl.relatedWorkPackage" ng-if="$ctrl.relatedWorkPackage">
<div wp-edit-field="'status'">
</div>
</div>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section">
<div ng-hide="!$ctrl.showRelationControls">
<accessible-by-keyboard ng-hide="$ctrl.relationType !== 'parent'"
execute="$ctrl.showEditForm = true">
<icon-wrapper icon-name="edit"
icon-title="{{::$ctrl.I18n.t('js.relation_buttons.change_parent')}}">
</icon-wrapper>
</accessible-by-keyboard>
<accessible-by-keyboard ng-hide="!$ctrl.relationType"
execute="$ctrl.removeRelation()">
<icon-wrapper icon-name="remove"
icon-title="{{::$ctrl.I18n.t('js.relation_buttons.remove')}}">
</icon-wrapper>
</accessible-by-keyboard>
</div>
</div>
</div>
<div ng-if="($ctrl.relationType === 'parent' && !$ctrl.relatedWorkPackage) || $ctrl.showEditForm">
<wp-relations-create template="empty-parents"
fixed-relation-type="parent"
external-form-toggle="$ctrl.showEditForm"
work-package="$ctrl.workPackage"></wp-relations-create>
</div>
</div>

@ -0,0 +1,130 @@
//-- 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 {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
export class WorkPackageRelationsHierarchyController {
public workPackage:WorkPackageResourceInterface;
public parent:WorkPackageResourceInterface;
public children:WorkPackageResourceInterface[] = [];
public showEditForm:boolean = false;
public workPackagePath = this.PathHelper.workPackagePath;
public canHaveChildren = !this.workPackage.isMilestone;
constructor(public I18n,
protected $scope:ng.IScope,
protected $rootScope:ng.IRootScopeService,
protected $q:ng.IQService,
protected PathHelper,
protected wpCacheService:WorkPackageCacheService) {
this.registerEventListeners();
if (angular.isNumber(this.workPackage.parentId)) {
this.loadParents();
}
if (this.workPackage.children) {
this.loadChildren();
}
}
protected loadParents() {
this.wpCacheService.loadWorkPackage(this.workPackage.parentId)
.take(1)
.subscribe((parent:WorkPackageResourceInterface) => {
this.parent = parent;
});
}
protected loadChildren() {
let relatedChildrenPromises = this.workPackage.children.map(child => child.$load());
this.$q.all(relatedChildrenPromises).then((children:Array<WorkPackageResourceInterface>) => {
this.children = children;
});
}
protected removedChild(evt, removedChild) {
_.remove(this.children, {'id' : removedChild.id});
this.wpCacheService.updateWorkPackageList([this.workPackage, removedChild]);
this.$rootScope.$emit('workPackagesRefreshInBackground');
}
protected addedChild(evt, addedChildWorkPackage) {
this.children.push(addedChildWorkPackage);
this.wpCacheService.updateWorkPackageList([this.workPackage, addedChildWorkPackage]);
this.$rootScope.$emit('workPackagesRefreshInBackground');
}
private registerEventListeners() {
this.$scope.$on('wp-relations.changedParent', this.updatedParent.bind(this));
this.$scope.$on('wp-relations.removedChild', this.removedChild.bind(this));
this.$scope.$on('wp-relations.addedChild', this.addedChild.bind(this));
}
private updatedParent(evt, changedData) {
if (changedData.parentId !== null) {
// parent changed
this.wpCacheService.loadWorkPackage(changedData.parentId, true)
.take(1)
.subscribe((parent:WorkPackageResourceInterface) => {
this.parent = parent;
this.wpCacheService.updateWorkPackageList([this.workPackage, parent]);
this.$rootScope.$emit('workPackagesRefreshInBackground');
});
} else {
// parent deleted
this.$rootScope.$emit('workPackagesRefreshInBackground');
this.parent = null;
}
this.workPackage = changedData.updatedWp;
}
}
function wpRelationsDirective() {
return {
restrict: 'E',
replace: true,
templateUrl: '/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html',
scope: {
workPackage: '=',
relationType: '@'
},
controller: WorkPackageRelationsHierarchyController,
controllerAs: '$ctrl',
bindToController: true,
};
}
wpDirectivesModule.directive('wpRelationsHierarchy', wpRelationsDirective);

@ -0,0 +1,89 @@
//-- 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 {wpDirectivesModule} from '../../../angular-modules';
export class WorkPackageRelationsHierarchyService {
constructor(protected $state,
protected $q,
protected wpCacheService) {
}
public changeParent(workPackage, parentId) {
workPackage.parentId = (parentId as number);
return workPackage.save();
}
public removeParent(workPackage) {
return this.changeParent(workPackage, null);
}
public addExistingChildWp(workPackage, childWpId) {
let deferred = this.$q.defer();
this.wpCacheService.loadWorkPackage([childWpId])
.take(1)
.subscribe(wpToBecomeChild => {
wpToBecomeChild.parentId = workPackage.id;
deferred.resolve(wpToBecomeChild.save());
});
return deferred.promise;
}
public addNewChildWp(workPackage) {
workPackage.project.$load()
.then(() => {
const args = [
'work-packages.list.new',
{
parent_id: workPackage.id,
projectPath: workPackage.project.identifier
}
];
if (this.$state.includes('work-packages.show')) {
args[0] = 'work-packages.new';
}
(<any>this.$state).go(...args);
});
}
public removeChild(childWorkPackage) {
childWorkPackage.parentId = null;
return childWorkPackage.save();
}
}
wpDirectivesModule.service('WpRelationsHierarchyService', WorkPackageRelationsHierarchyService);

@ -0,0 +1,19 @@
<div class="wp-relations-hierarchy-section">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
{{::this.$ctrl.I18n.t('js.relations_hierarchy.hierarchy_headline')}}
</h3>
</div>
</div>
<wp-relations-hierarchy-row work-package="$ctrl.workPackage" related-work-package="$ctrl.parent" relation-type="parent"></wp-relations-hierarchy-row>
<wp-relations-hierarchy-row work-package="$ctrl.workPackage" indent-by="20"></wp-relations-hierarchy-row>
<wp-relations-hierarchy-row work-package="$ctrl.workPackage" related-work-package="relatedWorkPackage" indent-by="40" relation-type="child" ng-repeat="relatedWorkPackage in $ctrl.children"></wp-relations-hierarchy-row>
<wp-relations-create
fixed-relation-type="children"
work-package="$ctrl.workPackage"
template="add-child"
ng-if="$ctrl.workPackage.addRelation && $ctrl.canHaveChildren"></wp-relations-create>
</div>

@ -1,93 +0,0 @@
<div ng-class="['relation', $ctrl.relationGroup.id]">
<h3>
<accessible-by-keyboard execute="$ctrl.toggleExpand()"
link-class="{{ $ctrl.relationGroup.id }}-toggle-link">
<i class="icon-pull-content" ng-class="$ctrl.stateClass"></i> {{ $ctrl.text.title }}
<span ng-if="$ctrl.relationGroup.id !== 'parent'">
({{ $ctrl.relationGroup.relations.length }})
</span>
</accessible-by-keyboard>
</h3>
<div class="content" ng-if="$ctrl.groupExpanded">
<div class="workpackages">
<div ng-if="!$ctrl.relationGroup.isEmpty">
<table class="attributes-table">
<colgroup>
<col style="width: 50%"/>
<col style="width: 15%"/>
<col/>
<col style="width: 1rem"/>
</colgroup>
<thead>
<tr>
<th title="{{ $ctrl.text.table.subject }}">{{ $ctrl.text.table.subject }}</th>
<th title="{{ $ctrl.text.table.status }}">{{ $ctrl.text.table.status }}</th>
<th title="{{ $ctrl.text.table.assignee }}">{{ $ctrl.text.table.assignee }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr wp-relation-row
ng-repeat="relation in $ctrl.relationGroup.relations">
<td focus="$ctrl.isFocused($index)">
<a title="{{ fullIdentifier }}" class="work_package" ng-class="state"
href="{{ workPackagePath(relatedWorkPackage.id) }}">
{{ fullIdentifier }}
</a>
</td>
<td title="{{ relatedWorkPackage.status.name }}">
{{ relatedWorkPackage.status.name }}
</td>
<td>
<a
ng-if="relatedWorkPackage.assignee
&& relatedWorkPackage.assignee.subtype != 'Group'"
title="{{ relatedWorkPackage.assignee.name }}"
href="{{ userPath(relatedWorkPackage.assignee.id) }}">
{{ relatedWorkPackage.assignee.name }}
</a>
<span ng-if="relatedWorkPackage.assignee
&& relatedWorkPackage.assignee.subtype == 'Group'">
{{ relatedWorkPackage.assignee.name }}
</span>
<empty-element ng-if="!relatedWorkPackage.assignee"></empty-element>
</td>
<td class="icon">
<accessible-by-keyboard ng-if="$ctrl.canRemoveRelation(relation)"
execute="$ctrl.removeRelation(relation)">
<icon-wrapper icon-name="remove"
icon-title="{{ $ctrl.text.relations.remove }}">
</icon-wrapper>
</accessible-by-keyboard>
</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="$ctrl.relationGroup.isEmpty">
{{ $ctrl.text.relations.empty }}
</div>
</div>
<div
class="add-relation"
ng-if="$ctrl.relationGroup.canAddRelation"
ng-switch="$ctrl.relationGroup.type"
focus="$ctrl.isFocused(-1)">
<!-- Add WP child -->
<button
ng-switch-when="children"
class="button add-work-package-child-button"
title="{{ $ctrl.btnTitle }}"
ng-bind-html="$ctrl.btnIcon + ' ' + $ctrl.btnTitle"
ng-click="$ctrl.addRelation()"
focus="$ctrl.isFocused(-1)"
></button>
<!-- Add WP relation -->
<add-wp-relation ng-switch-default></add-wp-relation>
</div>
</div>
</div>

@ -1,344 +0,0 @@
//-- 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 {wpTabsModule, opApiModule} from '../../angular-modules';
const expect = chai.expect;
describe('Work Package Relations Directive', () => {
var $q;
var I18n;
var compile;
var element;
var scope;
var stateParams = {};
var WorkPackageChildRelationsGroup;
var workPackage;
var relation;
var relationGroupMock;
var canAdd;
var canRemove;
beforeEach(angular.mock.module(
wpTabsModule.name,
opApiModule.name,
'openproject.helpers',
'openproject.models',
'openproject.layout',
'openproject.services',
'openproject.viewModels',
'ngSanitize'));
beforeEach(angular.mock.module('openproject.templates', function ($provide) {
var configurationService = {
isTimezoneSet: sinon.stub().returns(false),
accessibilityModeEnabled: sinon.stub().returns(false)
};
$provide.constant('$stateParams', stateParams);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(angular.mock.inject(($rootScope,
$compile,
_$q_,
_I18n_,
_WorkPackageChildRelationsGroup_) => {
$q = _$q_;
I18n = _I18n_;
WorkPackageChildRelationsGroup = _WorkPackageChildRelationsGroup_;
scope = $rootScope.$new();
compile = html => {
element = $compile(html)(scope);
scope.$digest();
};
relation = {
subject: 'Subject 1',
assignee: {
name: 'Assignee 1'
},
status: {
name: 'Status 1',
isClosed: false
},
$load: () => $q.when(relation)
};
workPackage = {};
canAdd = true;
canRemove = true;
relationGroupMock = {
id: 'parent',
type: 'parent',
name: 'parent',
relations: [relation],
getRelatedWorkPackage: () => relation.$load(),
canAddRelation: () => canAdd,
canRemoveRelation: () => canRemove,
isEmpty: false,
};
var stub = sinon.stub(I18n, 't');
stub.withArgs('js.work_packages.properties.subject').returns('Subject');
stub.withArgs('js.work_packages.properties.status').returns('Status');
stub.withArgs('js.work_packages.properties.assignee').returns('Assignee');
stub.withArgs('js.relations.remove').returns('Remove relation');
stub.withArgs('js.relation_labels.parent').returns('Parent');
stub.withArgs('js.relation_labels.something').returns('Something');
}));
afterEach(() => {
I18n.t.restore();
});
var html = `<wp-relations relation-group="relationGroup"
button-title="'Add Relation'"></wp-relations>`;
var shouldBehaveLikeRelationsDirective = () => {
it('should have a title', () => {
const title = angular.element(element.find('h3'));
const text = relationGroupMock.id === 'something' ? 'Something' : 'Parent';
expect(title.text()).to.include(text);
});
};
var shouldBehaveLikeHasTableHeader = () => {
it('should have a table head', () => {
var column0 = angular.element(element.find('.workpackages table thead th:nth-child(1)'));
var column1 = angular.element(element.find('.workpackages table thead th:nth-child(2)'));
var column2 = angular.element(element.find('.workpackages table thead th:nth-child(3)'));
expect(angular.element(column0).text()).to.eq(I18n.t('js.work_packages.properties.subject'));
expect(angular.element(column1).text()).to.eq(I18n.t('js.work_packages.properties.status'));
expect(angular.element(column2).text()).to.eq(I18n.t('js.work_packages.properties.assignee'));
});
};
var shouldBehaveLikeHasTableContent = (removable) => {
it('should have table content', () => {
let x = 1;
var column0 = element.find('.workpackages tr:nth-of-type(' + x + ') td:nth-child(1)');
var column1 = element.find('.workpackages tr:nth-of-type(' + x + ') td:nth-child(2)');
var column2 = element.find('.workpackages tr:nth-of-type(' + x + ') td:nth-child(3)');
expect(column0.text()).to.include('Subject ' + x);
expect(column1.text()).to.include('Status ' + x);
expect(column2.text()).to.include('Assignee ' + x);
expect(column0.find('a').hasClass('work_package')).to.be.true;
expect(column0.find('a').hasClass('closed')).to.be.false;
if (removable) {
const column4 = element.find('.workpackages table tbody tr:nth-of-type(' + x + ') td:nth-child(4)');
const removeIcon = column4.find('span.icon-remove');
expect(removeIcon.length).not.to.eq(0);
expect(removeIcon.attr('title')).to.include('Remove relation');
}
});
};
var shouldBehaveLikeCollapsedRelationsDirective = () => {
shouldBehaveLikeRelationsDirective();
it('should be initially collapsed', () => {
var content = angular.element(element.find('div.content'));
expect(content.hasClass('ng-if')).to.eq(false);
});
};
var shouldBehaveLikeExpandedRelationsDirective = () => {
shouldBehaveLikeRelationsDirective();
it('should be initially expanded', () => {
var content = angular.element(element.find('div.content'));
expect(content.hasClass('ng-hide')).to.eq(false);
});
};
var shouldBehaveLikeSingleRelationDirective = () => {
it('should NOT have an elements count', () => {
let len = scope.relationGroup.length;
expect(element.find('h3').text()).to.not.include('(' + len + ')');
});
};
var shouldBehaveLikeMultiRelationDirective = () => {
it('should have an elements count', () => {
let len = scope.relationGroup.relations.length;
expect(element.find('h3').text()).to.include('(' + len + ')');
});
};
var shouldBehaveLikeHasAddRelationDialog = () => {
it('should have an add relation button and id input', () => {
const addRelationDiv = element.find('.content .add-relation');
const button = addRelationDiv.find('button');
expect(addRelationDiv).not.to.eq(0);
expect(button.attr('title')).to.include('Add Relation');
expect(button.text()).to.include('Add Relation');
});
};
var shouldBehaveLikeReadOnlyRelationDialog = () => {
it('should have no add relation button and id input', () => {
var addRelationDiv = element.find('.workpackages .add-relation');
expect(addRelationDiv.length).to.eq(0);
});
};
describe('when having child relations', () => {
var childGroupConfig;
beforeEach(() => {
childGroupConfig = {
name: 'children',
type: 'children'
};
});
context('when it is possible to add a child relation', () => {
beforeEach(() => {
workPackage = {
addChild: true,
children: [relation],
};
scope.relationGroup =
new WorkPackageChildRelationsGroup(workPackage, childGroupConfig);
compile(html);
});
it('should have an "add child" button', () => {
expect(element.find('.add-work-package-child-button').length).to.eq(1);
});
});
context('when it is not possible to add a child relation', () => {
beforeEach(() => {
scope.relationGroup =
new WorkPackageChildRelationsGroup({}, childGroupConfig);
compile(html);
});
it('should have no add child link', () => {
expect(angular.element(element.find('.add-work-package-child-button')).length).to.eq(0);
});
});
});
describe('when there is no element markup', () => {
beforeEach(() => {
canAdd = true;
relationGroupMock.id = 'something';
scope.relationGroup = relationGroupMock;
compile(html);
});
shouldBehaveLikeMultiRelationDirective();
shouldBehaveLikeCollapsedRelationsDirective();
shouldBehaveLikeHasAddRelationDialog();
});
describe('single element markup', () => {
describe('header', () => {
beforeEach(() => {
scope.relationGroup = relationGroupMock;
compile(html);
});
shouldBehaveLikeSingleRelationDirective();
});
describe('when it is readonly', () => {
beforeEach(() => {
canRemove = true;
scope.relationGroup = relationGroupMock;
compile(html);
});
shouldBehaveLikeRelationsDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
shouldBehaveLikeHasTableContent(true);
shouldBehaveLikeReadOnlyRelationDialog();
});
describe('when it is possible to add and remove relations', () => {
beforeEach(() => {
scope.relationGroup = relationGroupMock;
compile(html);
});
shouldBehaveLikeRelationsDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
shouldBehaveLikeHasTableContent(false);
shouldBehaveLikeHasAddRelationDialog();
});
describe('when the work package is closed', () => {
beforeEach(() => {
relation.status.isClosed = true;
scope.relationGroup = relationGroupMock;
compile(html);
});
it('should have set the css class of the row to closed', () => {
var closedWorkPackageRow = element.find('.workpackages tr:nth-of-type(1) td:nth-child(1) a');
expect(closedWorkPackageRow.hasClass('closed')).to.be.true;
});
});
describe('when a table row has no work package assigned', () => {
var row;
beforeEach(() => {
relation.assignee = null;
scope.relationGroup = relationGroupMock;
compile(html);
row = element.find('.workpackages tr:nth-of-type(1)');
});
it('should NOT have link', () => {
expect(row.find('td:nth-of-type(2) a').length).to.eql(0);
});
it('should have empty element tag', () => {
expect(row.find('empty-element').text()).to.include('-');
});
});
});
});

@ -26,104 +26,130 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {wpTabsModule} from '../../angular-modules';
import {WorkPackageRelationGroup} from './wp-relation-group/wp-relation-group.service';
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
import {WorkPackageResource} from '../api/api-v3/hal-resources/work-package-resource.service';
import {wpDirectivesModule} from "../../angular-modules";
import {RelatedWorkPackage, RelatedWorkPackagesGroup} from './wp-relations.interfaces';
const iconArrowClasses = ['icon-arrow-up1', 'icon-arrow-down1'];
import {WorkPackageResourceInterface} from '../api/api-v3/hal-resources/work-package-resource.service';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
export class WorkPackageRelationsController {
public btnTitle:string;
public btnIcon:string = '<i class="icon-hierarchy icon-add"></i>';
public workPackage:WorkPackageResource;
public relationGroup:WorkPackageRelationGroup;
public focusElementIndex:number = -2;
public wpToAddId:number = null;
public expand:boolean = false;
public text:any;
private _initialExpand:boolean;
public get stateClass():string {
return iconArrowClasses[+!!this.expand];
public relationGroups:RelatedWorkPackagesGroup;
public workPackage:WorkPackageResourceInterface;
public currentRelations: Array<RelatedWorkPackage> = Array();
private sortByWpType:boolean;
constructor(protected $scope:ng.IScope,
protected $q:ng.IQService,
protected $state,
protected wpCacheService:WorkPackageCacheService) {
this.registerEventListeners();
if (this.workPackage.relations) {
if (!this.workPackage.relations.$loaded) {
this.workPackage.relations.$load().then(() => {
if ((this.workPackage.relations as any).count > 0) {
this.loadRelations();
}
});
} else {
if ((this.workPackage.relations as any).count > 0) {
this.loadRelations();
}
}
}
}
public get groupExpanded() {
if (angular.isUndefined(this._initialExpand) && !this.relationGroup.isEmpty) {
this._initialExpand = this.expand = !this.relationGroup.isEmpty;
}
return this.expand;
protected removeSingleRelation(evt, relation) {
this.currentRelations = _.remove(this.currentRelations, (latestRelation) => {
return latestRelation.relatedBy.href !== relation.href;
});
this.buildRelationGroups();
}
constructor(protected $scope,
protected I18n,
protected wpNotificationsService:WorkPackageNotificationService,
protected NotificationsService) {
this.text = {
title: I18n.t('js.relation_labels.' + this.relationGroup.id),
table: {
subject: I18n.t('js.work_packages.properties.subject'),
status: I18n.t('js.work_packages.properties.status'),
assignee: I18n.t('js.work_packages.properties.assignee')
},
relations: {
empty: I18n.t('js.relations.empty'),
remove: I18n.t('js.relations.remove')
}
};
if (this.relationGroup.id === 'parent') {
this.btnIcon = '<i class="icon-hierarchy icon-edit"></i>';
}
}
protected getRelatedWorkPackages(workPackageIds:Array<any>) {
let observablesToGetZipped = workPackageIds.map(wpId => this.wpCacheService.loadWorkPackage(wpId));
public addRelation() {
this.relationGroup.addWpRelation(this.wpToAddId)
.then(() => {
this.wpToAddId = null;
this.handleSuccess(-1);
})
.catch(error => this.wpNotificationsService.handleErrorResponse(error, this.workPackage));
if (observablesToGetZipped.length > 1) {
return Rx.Observable
.zip(observablesToGetZipped)
.take(1);
} else {
return observablesToGetZipped[0].take(1);
}
}
public canRemoveRelation(relation?):boolean {
return this.relationGroup.canRemoveRelation(relation);
protected getRelatedWorkPackageId(relation) {
if (relation.relatedTo.href === this.workPackage.href) {
return parseInt(relation.relatedFrom.href.split('/').pop());
} else {
return parseInt(relation.relatedTo.href.split('/').pop());
}
}
public removeRelation(relation) {
this.relationGroup.removeWpRelation(relation)
.then(index => {
this.handleSuccess(index);
})
.catch(error => this.wpNotificationsService.handleErrorResponse(error, this.workPackage));
protected buildRelationGroups() {
if (angular.isDefined(this.currentRelations)) {
this.relationGroups = (_.groupBy(this.currentRelations, (wp) => {
return wp.type.name;
}) as Array<any>);
}
}
public toggleExpand() {
this.expand = !this.expand;
public resortRelations() {
if (angular.isDefined(this.currentRelations)) {
this.relationGroups = (_.groupBy(this.currentRelations, (wp) => {
if (this.sortByWpType) {
return wp.type.name;
}
else {
return wp.relatedBy._type;
}
}) as Array<any>);
this.sortByWpType = !this.sortByWpType;
}
}
public isFocused(index:number) {
return index === this.focusElementIndex;
protected addSingleRelation(evt, relation) {
var relatedWorkPackageId = [this.getRelatedWorkPackageId(relation)];
this.getRelatedWorkPackages(relatedWorkPackageId).take(1).subscribe((relatedWorkPackage:RelatedWorkPackage) => {
relatedWorkPackage.relatedBy = relation;
this.currentRelations.push(relatedWorkPackage);
this.buildRelationGroups();
});
}
public updateFocus(index:number) {
var length = this.relationGroup.relations.length;
if (length == 0) {
this.focusElementIndex = -1;
}
else {
this.focusElementIndex = (index < length) ? index : length - 1;
}
this.$scope.$evalAsync(() => this.$scope.$broadcast('updateFocus'));
protected loadRelations():void {
var relatedWpIds = [];
var relations = [];
this.workPackage.relations.elements.forEach(relation => {
const relatedWpId = this.getRelatedWorkPackageId(relation);
relatedWpIds.push(relatedWpId);
relations[relatedWpId] = relation;
});
this.getRelatedWorkPackages(relatedWpIds)
.subscribe(relatedWorkPackages => {
if (angular.isArray(relatedWorkPackages)) {
relatedWorkPackages.forEach(relatedWorkPackage => {
relatedWorkPackage.relatedBy = relations[relatedWorkPackage.id];
this.currentRelations = relatedWorkPackages;
});
}
else {
relatedWorkPackages.relatedBy = relations[relatedWorkPackages.id];
this.currentRelations[0] = relatedWorkPackages;
}
this.buildRelationGroups();
});
}
private handleSuccess(index) {
this.updateFocus(index);
this.$scope.$emit('workPackagesRefreshInBackground');
private registerEventListeners() {
this.$scope.$on('wp-relations.added', this.addSingleRelation.bind(this));
this.$scope.$on('wp-relations.removed', this.removeSingleRelation.bind(this));
}
}
@ -131,18 +157,16 @@ function wpRelationsDirective() {
return {
restrict: 'E',
replace: true,
templateUrl: '/components/wp-relations/wp-relations.directive.html',
templateUrl: '/components/wp-relations/wp-relations.template.html',
scope: {
relationGroup: '=',
workPackage: '=',
btnTitle: '=buttonTitle',
workPackage: '='
},
controller: WorkPackageRelationsController,
controllerAs: '$ctrl',
bindToController: true,
bindToController: true
};
}
wpTabsModule.directive('wpRelations', wpRelationsDirective);
wpDirectivesModule.directive('wpRelations', wpRelationsDirective);

@ -0,0 +1,28 @@
import {WorkPackageResourceInterface} from '../api/api-v3/hal-resources/work-package-resource.service';
import {HalResource} from '../api/api-v3/hal-resources/hal-resource.service';
export interface RelatedWorkPackage extends WorkPackageResourceInterface {
relatedBy: RelationResource;
}
export interface RelatedWorkPackagesGroup {
[key: string] : Array<RelatedWorkPackage>;
}
export interface RelationResource extends HalResource {
_type: string;
description: string;
updateRelation(params:Object): ng.IPromise<any>;
remove(): ng.IPromise<any>;
}
export interface RelationType {
name: string;
id?: string;
type: string;
}
export interface RelationTitle {
[key: string]: string;
}

@ -26,51 +26,88 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {wpTabsModule} from '../../angular-modules';
import {wpDirectivesModule} from '../../angular-modules';
import {WorkPackageResourceInterface} from '../api/api-v3/hal-resources/work-package-resource.service';
import {RelationTitle} from "./wp-relations.interfaces";
export interface WorkPackageRelationsConfigInterface {
name:string;
type:string;
id?:string;
}
export class WorkPackageRelationsService {
private relationsConfig:WorkPackageRelationsConfigInterface[] = [
{name: 'parent', type: 'parent'},
{name: 'children', type: 'children'},
{name: 'relatedTo', type: 'Relation::Relates', id: 'relates'},
{name: 'duplicates', type: 'Relation::Duplicates'},
{name: 'duplicated', type: 'Relation::Duplicated'},
{name: 'blocks', type: 'Relation::Blocks'},
{name: 'blocked', type: 'Relation::Blocked'},
{name: 'precedes', type: 'Relation::Precedes'},
{name: 'follows', type: 'Relation::Follows'}
];
constructor(protected WorkPackageRelationGroup,
protected WorkPackageParentRelationGroup,
protected WorkPackageChildRelationsGroup) {
constructor(protected $rootScope,
protected $q,
protected $state,
protected I18n,
protected wpCacheService,
protected wpNotificationsService,
protected NotificationsService) {
}
public getWpRelationGroups(workPackage:WorkPackageResourceInterface) {
let configsOfInterest = this.relationsConfig;
if (workPackage.isMilestone) {
configsOfInterest = _.reject(configsOfInterest, {name: 'children'});
}
return configsOfInterest.map(config => {
switch (config.type) {
case 'parent':
return new this.WorkPackageParentRelationGroup(workPackage, config);
case 'children':
return new this.WorkPackageChildRelationsGroup(workPackage, config);
default:
return new this.WorkPackageRelationGroup(workPackage, config);
}
});
public addCommonRelation(workPackage, relationType, relatedWpId) {
const params = {
to_id: relatedWpId,
relation_type: relationType
};
return workPackage.addRelation(params);
}
public changeRelationDescription(relation, description) {
const params = {
description: description
};
return relation.update(params);
}
public changeRelationType(relation, relationType) {
const params = {
relation_type: relationType
};
return relation.update(params);
}
public removeCommonRelation(relation, workPackage) {
return relation.remove();
}
public getTranslatedRelationTitle(relationTypeName:string) {
return this.getRelationTypeObjectByName(relationTypeName).label;
}
public getRelationTypeObjectByType(type:string) {
return _.find(this.configuration.relationTypes, {type: type});
}
public getRelationTypeObjectByName(name:string) {
return _.find(this.configuration.relationTypes, {name: name});
}
public getRelationTypes(rejectParentChild?:boolean) {
let relationTypes = angular.copy(this.configuration.relationTypes);
if (rejectParentChild) {
_.remove(relationTypes, (relationType) => {
return relationType.name === 'parent' || relationType.name === 'children';
});
}
return relationTypes;
}
public configuration = {
relationTypes: [
{name: 'parent', type: 'parent', label: this.I18n.t('js.relation_labels.parent')},
{name: 'children', type: 'children', label: this.I18n.t('js.relation_labels.children')},
{name: 'relatedTo', type: 'Relation::Relates', id: 'relates', label: this.I18n.t('js.relation_labels.relates')},
{name: 'duplicates', type: 'Relation::Duplicates', label: this.I18n.t('js.relation_labels.duplicates')},
{name: 'duplicated', type: 'Relation::Duplicated', label: this.I18n.t('js.relation_labels.duplicated')},
{name: 'blocks', type: 'Relation::Blocks', label: this.I18n.t('js.relation_labels.blocks')},
{name: 'blocked', type: 'Relation::Blocked', label: this.I18n.t('js.relation_labels.blocked')},
{name: 'precedes', type: 'Relation::Precedes', label: this.I18n.t('js.relation_labels.precedes')},
{name: 'follows', type: 'Relation::Follows', label: this.I18n.t('js.relation_labels.follows')}
]
};
}
wpTabsModule.service('wpRelations', WorkPackageRelationsService);
wpDirectivesModule.service('WpRelationsService', WorkPackageRelationsService);

@ -0,0 +1,15 @@
<div>
<div ng-repeat="(type, relatedWorkPackages) in $ctrl.relationGroups">
<wp-relations-group wp-type="type"
related-work-packages="relatedWorkPackages"
work-package="$ctrl.workPackage"></wp-relations-group>
</div>
<wp-relations-create related-work-packages="$ctrl.currentRelations"
work-package="$ctrl.workPackage"
template="dynamic-relation-types"
ng-if="$ctrl.workPackage.addRelation"></wp-relations-create>
<wp-relations-hierarchy work-package="$ctrl.workPackage"></wp-relations-hierarchy>
</div>

@ -0,0 +1,60 @@
//-- 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 {wpDirectivesModule} from '../../angular-modules';
import {WorkPackageResourceInterface} from '../api/api-v3/hal-resources/work-package-resource.service';
import {RelationResource} from './wp-relations.interfaces';
/**
* Contains methods and attributes shared
* between common relations and parent-child relations
*/
export class WorkPackageSingleRelationController {
public workPackagePath = this.PathHelper.workPackagePath;
constructor(protected PathHelper) {
}
public getFullIdentifier(workPackage:WorkPackageResourceInterface, hideType?:boolean) {
var type = '';
if (workPackage.type && !hideType) {
type += workPackage.type.name + ': ';
}
return `${type}${workPackage.subject}`;
}
}
function wpSingleRelationDirective() {
return {
restrict: 'A',
controller: WorkPackageSingleRelationController,
controllerAs: '$singleRelation',
bindToController: true,
};
}
wpDirectivesModule.directive('wpSingleRelation', wpSingleRelationDirective);
Loading…
Cancel
Save