Make relations related components use the HalResource API

pull/4471/head
Alex Dik 9 years ago
parent e8041c2957
commit c8faeb43fe
  1. 14
      frontend/app/components/wp-panels/relations-panel/relations-panel.directive.ts
  2. 12
      frontend/app/components/wp-relations/add-wp-relation/add-wp-relation.directive.html
  3. 15
      frontend/app/components/wp-relations/add-wp-relation/add-wp-relation.directive.ts
  4. 8
      frontend/app/components/wp-relations/parent-relations-handler/parent-relations-handler.service.ts
  5. 58
      frontend/app/components/wp-relations/relations-handler/relations-handler.service.ts
  6. 43
      frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.directive.ts
  7. 111
      frontend/app/components/wp-relations/wp-relations.directive.html
  8. 2
      frontend/app/components/wp-relations/wp-relations.directive.test.ts
  9. 109
      frontend/app/components/wp-relations/wp-relations.directive.ts
  10. 68
      frontend/app/work_packages/helpers/work-packages-helper.js

@ -38,6 +38,8 @@ export class RelationsPanelController {
RelationsHandler, RelationsHandler,
ChildRelationsHandler, ChildRelationsHandler,
ParentRelationsHandler) { ParentRelationsHandler) {
$scope.wpParent = new ParentRelationsHandler(this.workPackage);
$scope.wpChildren = new ChildRelationsHandler(this.workPackage);
if (this.workPackage.parent) { if (this.workPackage.parent) {
this.workPackage.parent.$load().then(parent => { this.workPackage.parent.$load().then(parent => {
@ -49,11 +51,13 @@ export class RelationsPanelController {
$scope.wpChildren = new ChildRelationsHandler(this.workPackage, this.workPackage.children); $scope.wpChildren = new ChildRelationsHandler(this.workPackage, this.workPackage.children);
} }
angular.forEach(RELATION_TYPES, (type, identifier) => { if (Array.isArray(this.workPackage.relations)) {
var relations = this.workPackage.relations.filter(relation => relation._type === type); angular.forEach(RELATION_TYPES, (type, identifier) => {
var relationId = RELATION_IDENTIFIERS[identifier]; var relations = this.workPackage.relations.filter(relation => relation._type === type);
$scope[identifier] = new RelationsHandler(this.workPackage, relations, relationId); var relationId = RELATION_IDENTIFIERS[identifier];
}); $scope[identifier] = new RelationsHandler(this.workPackage, relations, relationId);
});
}
} }
} }

@ -2,10 +2,10 @@
<div class="choice"> <div class="choice">
<div class="choice--select"> <div class="choice--select">
<ui-select <ui-select
id="relation_to_id-{{ handler.relationsId }}" id="relation_to_id-{{ $ctrl.handler.relationsId }}"
name="relation[to_id][{{ handler.relationsId }}]" name="relation[to_id][{{ $ctrl.handler.relationsId }}]"
ng-model="$parent.relationToAddId" ng-model="$parent.relationToAddId"
title="{{ I18n.t('js.field_value_enter_prompt', { field: I18n.t('js.relation_labels.' + handler.relationsId) }) }}" title="{{ text.uiSelectTitle }}"
required required
theme="select2" theme="select2"
tabindex="0"> tabindex="0">
@ -20,9 +20,9 @@
</div> </div>
<div class="choice--button"> <div class="choice--button">
<button class="button" <button class="button"
title="{{ btnTitle }}" title="{{ $ctrl.btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle" ng-bind-html="$ctrl.btnIcon + ' ' + $ctrl.btnTitle"
ng-click="handler.addRelation(this)" ng-click="$ctrl.handler.addRelation(this)"
ng-disabled="add_relation_form.$invalid"> ng-disabled="add_relation_form.$invalid">
</button> </button>
</div> </div>

@ -27,15 +27,22 @@
//++ //++
import {wpTabsModule} from "../../../angular-modules"; import {wpTabsModule} from "../../../angular-modules";
import {WorkPackageRelationsController} from "../wp-relations.directive";
declare const URI; declare const URI;
function addWpRelationDirective($http, PathHelper) { function addWpRelationDirective($http, PathHelper, I18n) {
return { return {
restrict: 'E', restrict: 'E',
templateUrl: '/components/wp-relations/add-wp-relation/add-wp-relation.directive.html', templateUrl: '/components/wp-relations/add-wp-relation/add-wp-relation.directive.html',
require: '^wpRelations',
link: function (scope) { link: function (scope, element, attrs, relationsCtrl:WorkPackageRelationsController) {
scope.text = {
uiSelectTitle: I18n.t('js.field_value_enter_prompt', {
field: I18n.t('js.relation_labels.' + relationsCtrl.handler.relationsId)
})
};
scope.relationToAddId = null; scope.relationToAddId = null;
scope.autocompleteWorkPackages = function (term) { scope.autocompleteWorkPackages = function (term) {
if (!term) return; if (!term) return;
@ -44,8 +51,8 @@ function addWpRelationDirective($http, PathHelper) {
q: term, q: term,
scope: 'relatable', scope: 'relatable',
escape: false, escape: false,
id: scope.handler.workPackage.props.id, id: relationsCtrl.handler.workPackage.id,
'project_id': scope.handler.workPackage.embedded.project.props.id project_id: relationsCtrl.handler.workPackage.project.id
}; };
return $http({ return $http({

@ -34,12 +34,10 @@ var ApiNotificationsService:any;
var PathHelper:any; var PathHelper:any;
export class ParentRelationsHandler extends RelationsHandler { export class ParentRelationsHandler extends RelationsHandler {
public type = 'parent'; public type:string = 'parent';
public isSingletonRelation = true;
constructor(workPackage, parent, relationsId) {
constructor(workPackage, parents, relationsId) { super(workPackage, parent && [parent], relationsId);
super(workPackage, parents.filter(parent => parent.id !== workPackage.id), relationsId);
} }
public removeRelation(scope) { public removeRelation(scope) {

@ -35,7 +35,6 @@ var ApiNotificationsService:any;
export class RelationsHandler { export class RelationsHandler {
public type:string = 'relation'; public type:string = 'relation';
public isSingletonRelation:boolean = false;
constructor(public workPackage:WorkPackageResourceInterface, public relations, public relationsId) { constructor(public workPackage:WorkPackageResourceInterface, public relations, public relationsId) {
} }
@ -45,51 +44,48 @@ export class RelationsHandler {
} }
public getCount() { public getCount() {
return this.relations ? this.relations.length : 0; return this.relations && this.relations.length || 0;
} }
public canAddRelation() { public canAddRelation() {
return !!this.workPackage.$links.addRelation; return !!this.workPackage.addRelation;
} }
public canDeleteRelation(relation) { public canDeleteRelation(relation) {
return !!relation.$links.remove; return !!relation.remove;
} }
public addRelation(scope) { public addRelation(scope) {
WorkPackageService.addWorkPackageRelation(this.workPackage, // WorkPackageService.addWorkPackageRelation(this.workPackage,
scope.relationToAddId, // scope.relationToAddId,
this.relationsId) // this.relationsId)
.then(function () { // .then(function () {
scope.relationToAddId = ''; // scope.relationToAddId = '';
scope.updateFocus(-1); // scope.updateFocus(-1);
scope.$emit('workPackageRefreshRequired'); // scope.$emit('workPackageRefreshRequired');
}, function (error) { // }, function (error) {
ApiNotificationsService.addError(error); // ApiNotificationsService.addError(error);
}); // });
} }
public removeRelation(scope) { public removeRelation(scope) {
var index = this.relations.indexOf(scope.relation); // var index = this.relations.indexOf(scope.relation);
var handler = this; // var handler = this;
//
WorkPackageService.removeWorkPackageRelation(scope.relation).then(() => { // WorkPackageService.removeWorkPackageRelation(scope.relation).then(() => {
handler.relations.splice(index, 1); // handler.relations.splice(index, 1);
scope.updateFocus(index); // scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired'); // scope.$emit('workPackageRefreshRequired');
}, function (error) { // }, function (error) {
ApiNotificationsService.addError(scope, error); // ApiNotificationsService.addError(scope, error);
}); // });
} }
public getRelatedWorkPackage(workPackage, relation) { public getRelatedWorkPackage(relation) {
var self = workPackage.links.self.href; if (relation.relatedTo.href === this.workPackage.href) {
return relation.relatedFrom.$load();
if (relation.links.relatedTo.href === self) {
return relation.links.relatedFrom.fetch();
} else {
return relation.links.relatedTo.fetch();
} }
return relation.relatedTo.$load();
} }
} }

@ -27,22 +27,41 @@
//++ //++
import {wpTabsModule} from "../../../angular-modules"; 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 + ': ';
}
return `#${workPackage.id}${type}${workPackage.subject}`;
};
function wpRelationsDirectiveLink(scope,
element,
attrs,
relationsCtrl:WorkPackageRelationsController) {
scope.workPackagePath = PathHelper.workPackagePath;
scope.userPath = PathHelper.userPath;
relationsCtrl.handler.getRelatedWorkPackage(scope.relation)
.then(relatedWorkPackage => {
scope.relatedWorkPackage = relatedWorkPackage;
scope.fullIdentifier = getFullIdentifier(relatedWorkPackage);
scope.state = relatedWorkPackage.status.isClosed ? 'closed' : '';
});
}
function wpRelationRowDirective(PathHelper, WorkPackagesHelper) {
return { return {
restrict: 'A', restrict: 'A',
require: '^wpRelations',
link: (scope) => { link: wpRelationsDirectiveLink
scope.workPackagePath = PathHelper.workPackagePath;
scope.userPath = PathHelper.userPath;
scope.handler.getRelatedWorkPackage(scope.workPackage, scope.relation)
.then(relatedWorkPackage => {
scope.relatedWorkPackage = relatedWorkPackage;
scope.fullIdentifier = WorkPackagesHelper.getFullIdentifier(relatedWorkPackage);
scope.state = WorkPackagesHelper.getState(relatedWorkPackage);
});
}
}; };
} }

@ -1,73 +1,88 @@
<div ng-class="['relation', relationType]"> <div ng-class="['relation', $ctrl.relationType]">
<h3> <h3>
<accessible-by-keyboard execute="toggleExpand()" <accessible-by-keyboard execute="$ctrl.toggleExpand()"
link-class="{{ relationType }}-toggle-link"> link-class="{{ $ctrl.relationType }}-toggle-link">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }} <i class="icon-pull-content" ng-class="$ctrl.stateClass"></i> {{ $ctrl.text.title }}
<span ng-if="!handler.isSingletonRelation">({{ handler.getCount() }})</span> <span ng-if="$ctrl.relationType !== 'parent'">
({{ $ctrl.handler.getCount() }})
</span>
</accessible-by-keyboard> </accessible-by-keyboard>
</h3> </h3>
<div class="content" ng-show="expand"> <div class="content" ng-show="$ctrl.expand">
<div class="workpackages"> <div class="workpackages">
<div ng-if="handler.relations"> <div ng-if="handler.relations">
<table class="attributes-table"> <table class="attributes-table">
<colgroup> <colgroup>
<col style="width: 50%" /> <col style="width: 50%"/>
<col style="width: 15%" /> <col style="width: 15%"/>
<col /> <col/>
<col style="width: 1rem" /> <col style="width: 1rem"/>
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<td title="{{ I18n.t('js.work_packages.properties.subject')}}">{{ I18n.t('js.work_packages.properties.subject') }}</td> <td title="{{ $ctrl.text.table.subject }}">{{ $ctrl.text.table.subject }}</td>
<td title="{{ I18n.t('js.work_packages.properties.status')}}">{{ I18n.t('js.work_packages.properties.status') }}</td> <td title="{{ $ctrl.text.table.status }}">{{ $ctrl.text.table.status }}</td>
<td title="{{ I18n.t('js.work_packages.properties.assignee')}}">{{ I18n.t('js.work_packages.properties.assignee') }}</td> <td title="{{ $ctrl.text.table.assignee }}">{{ $ctrl.text.table.assignee }}</td>
<td></td> <td></td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr wp-relation-row <tr wp-relation-row
ng-repeat="relation in handler.relations"> ng-repeat="relation in $ctrl.handler.relations">
<td focus="isFocused($index)"> <td focus="$ctrl.isFocused($index)">
<a title="{{ fullIdentifier }}" class="work_package" ng-class="state" href="{{ workPackagePath(relatedWorkPackage.props.id) }}"> <a title="{{ fullIdentifier }}" class="work_package" ng-class="state"
{{ fullIdentifier }} href="{{ workPackagePath(relatedWorkPackage.id) }}">
</a> {{ fullIdentifier }}
</td> </a>
<td title="{{ relatedWorkPackage.embedded.status.props.name }}">{{ relatedWorkPackage.embedded.status.props.name }}</td> </td>
<td> <td title="{{ relatedWorkPackage.status.name }}">
<a ng-if="relatedWorkPackage.embedded.assignee && relatedWorkPackage.embedded.assignee.props.subtype != 'Group'" title="{{ relatedWorkPackage.embedded.assignee.props.name }}" href="{{ userPath(relatedWorkPackage.embedded.assignee.props.id) }}"> {{ relatedWorkPackage.status.name }}
{{ relatedWorkPackage.embedded.assignee.props.name }} </td>
</a> <td>
<span ng-if="relatedWorkPackage.embedded.assignee && relatedWorkPackage.embedded.assignee.props.subtype == 'Group'"> <a
{{ relatedWorkPackage.embedded.assignee.props.name }} 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> </span>
<empty-element ng-if="!relatedWorkPackage.embedded.assignee"></empty-element> <empty-element ng-if="!relatedWorkPackage.assignee"></empty-element>
</td> </td>
<td class="icon"> <td class="icon">
<accessible-by-keyboard ng-if="handler.canDeleteRelation(relation)" <accessible-by-keyboard ng-if="$ctrl.handler.canDeleteRelation(relation)"
execute="handler.removeRelation(this)"> execute="$ctrl.handler.removeRelation(this)">
<icon-wrapper icon-name="remove" <icon-wrapper icon-name="remove"
icon-title="{{ I18n.t('js.relations.remove') }}"> icon-title="{{ $ctrl.text.relations.remove }}">
</icon-wrapper> </icon-wrapper>
</accessible-by-keyboard> </accessible-by-keyboard>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div ng-if="handler.isEmpty()"> <div ng-if="$ctrl.handler.isEmpty()">
{{ I18n.t('js.relations.empty') }} {{ $ctrl.text.relations.empty }}
</div> </div>
</div> </div>
<div class="add-relation" ng-if="handler.canAddRelation()" ng-switch="handler.type" focus="isFocused(-1)"> <div
class="add-relation"
ng-if="$ctrl.handler.canAddRelation()"
ng-switch="$ctrl.handler.type"
focus="$ctrl.isFocused(-1)"
>
<!-- Add WP child --> <!-- Add WP child -->
<button <button
ng-switch-when="child" ng-switch-when="child"
class="button add-work-package-child-button" class="button add-work-package-child-button"
title="{{ btnTitle }}" title="{{ $ctrl.btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle" ng-bind-html="$ctrl.btnIcon + ' ' + $ctrl.btnTitle"
ng-click="handler.addRelation()" ng-click="$ctrl.handler.addRelation()"
focus="focusElementIndex === -1" focus="$ctrl.focusElementIndex === -1"
></button> ></button>

@ -407,8 +407,6 @@ describe('Work Package Relations Directive', function () {
describe('header', function () { describe('header', function () {
beforeEach(inject(function ($timeout) { beforeEach(inject(function ($timeout) {
scope.relations = relationsHandlerSingle; scope.relations = relationsHandlerSingle;
scope.relations.isSingletonRelation = true;
compile(html); compile(html);
$timeout.flush(); $timeout.flush();

@ -27,8 +27,69 @@
//++ //++
import {wpTabsModule} from "../../angular-modules"; import {wpTabsModule} from "../../angular-modules";
import {RelationsHandler} from "./relations-handler/relations-handler.service";
function wpRelationsDirective(I18n, WorkPackagesHelper, $timeout) { const iconArrowSuffixes = ['up1', 'down1'];
export class WorkPackageRelationsController {
public relationType:string;
public handler:RelationsHandler;
public btnTitle:string;
public btnIcon:string;
public focusElementIndex:number = -2;
public text:any;
public expand:boolean = false;
public get stateClass():string {
return 'icon-arrow-' + iconArrowSuffixes[+!!this.expand];
}
constructor(protected $scope, protected I18n) {
this.text = {
title: I18n.t('js.relation_labels.' + this.relationType),
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')
}
};
$scope.$watch('$ctrl.handler', () => {
if (this.handler) {
this.expand = this.expand || !this.handler.isEmpty();
}
});
}
public toggleExpand() {
this.expand = !this.expand;
}
public isFocused(index:number) {
return index === this.focusElementIndex;
}
public updateFocus(index:number) {
var length = this.handler.relations.length;
if (length == 0) {
this.focusElementIndex = -1;
}
else {
this.focusElementIndex = (index < length) ? index : length - 1;
}
this.$scope.$evalAsync(() => this.$scope.$broadcast('updateFocus'));
}
}
function wpRelationsDirective() {
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
@ -38,50 +99,12 @@ function wpRelationsDirective(I18n, WorkPackagesHelper, $timeout) {
relationType: '@', relationType: '@',
handler: '=', handler: '=',
btnTitle: '@buttonTitle', btnTitle: '@buttonTitle',
btnIcon: '@buttonIcon', btnIcon: '@buttonIcon'
isSingletonRelation: '@singletonRelation'
}, },
link: function(scope) { controller: WorkPackageRelationsController,
scope.I18n = I18n; controllerAs: '$ctrl',
scope.focusElementIndex = -2; bindToController: true,
scope.title = I18n.t('js.relation_labels.' + scope.relationType);
scope.$watch('handler', function() {
if (scope.handler) {
scope.workPackage = scope.handler.workPackage;
scope.expand = scope.expand || !scope.handler.isEmpty();
scope.relationsCount = scope.handler.getCount();
}
});
scope.$watch('expand', function(newVal, oldVal) {
scope.stateClass = WorkPackagesHelper.collapseStateIcon(newVal);
});
scope.toggleExpand = function() {
scope.expand = !scope.expand;
};
scope.isFocused = function(index) {
return index == scope.focusElementIndex;
};
scope.updateFocus = function(index) {
var length = scope.handler.relations.length;
if (length == 0) {
scope.focusElementIndex = -1;
} else {
scope.focusElementIndex = (index < length) ? index : length - 1;
}
$timeout(function() {
scope.$broadcast('updateFocus');
});
};
}
}; };
} }

@ -26,8 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
/* jshint camelcase: false */
module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) {
var WorkPackagesHelper = { var WorkPackagesHelper = {
@ -143,72 +141,6 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) {
parseDateTime: function(value) { parseDateTime: function(value) {
return new Date(Date.parse(value.replace(/(A|P)M$/, ''))); return new Date(Date.parse(value.replace(/(A|P)M$/, '')));
},
getParent: function(workPackage) {
var wpParent = workPackage.links.parent;
return (wpParent) ? [wpParent.fetch()] : [];
},
getChildren: function(workPackage) {
var children = workPackage.links.children;
var result = [];
if (children) {
for (var x = 0; x < children.length; x++) {
var child = children[x];
result.push(child);
}
}
return result;
},
getRelationsOfType: function(workPackage, type) {
var relations = workPackage.embedded.relations;
var result = [];
if (relations) {
for (var x = 0; x < relations.length; x++) {
var relation = relations[x];
if (relation.props._type == type) {
result.push(relation);
}
}
}
return result;
},
//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) {
return (workPackage.embedded.status.props.isClosed) ? 'closed' : '';
},
getFullIdentifier: function(workPackage) {
var id = '#' + workPackage.props.id;
if (workPackage.props.type) {
id += ' ' + workPackage.props.type + ':';
}
id += ' ' + workPackage.props.subject;
return id;
},
collapseStateIcon: function(collapsed) {
var iconClass = 'icon-arrow-';
if (collapsed) {
iconClass += 'up1';
} else {
iconClass += 'down1';
}
return iconClass;
} }
}; };

Loading…
Cancel
Save