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

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

@ -27,15 +27,22 @@
//++
import {wpTabsModule} from "../../../angular-modules";
import {WorkPackageRelationsController} from "../wp-relations.directive";
declare const URI;
function addWpRelationDirective($http, PathHelper) {
function addWpRelationDirective($http, PathHelper, I18n) {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/add-wp-relation/add-wp-relation.directive.html',
link: function (scope) {
require: '^wpRelations',
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.autocompleteWorkPackages = function (term) {
if (!term) return;
@ -44,8 +51,8 @@ function addWpRelationDirective($http, PathHelper) {
q: term,
scope: 'relatable',
escape: false,
id: scope.handler.workPackage.props.id,
'project_id': scope.handler.workPackage.embedded.project.props.id
id: relationsCtrl.handler.workPackage.id,
project_id: relationsCtrl.handler.workPackage.project.id
};
return $http({

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

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

@ -27,22 +27,41 @@
//++
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 {
restrict: 'A',
require: '^wpRelations',
link: (scope) => {
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);
});
}
link: wpRelationsDirectiveLink
};
}

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

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

@ -27,8 +27,69 @@
//++
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 {
restrict: 'E',
replace: true,
@ -38,50 +99,12 @@ function wpRelationsDirective(I18n, WorkPackagesHelper, $timeout) {
relationType: '@',
handler: '=',
btnTitle: '@buttonTitle',
btnIcon: '@buttonIcon',
isSingletonRelation: '@singletonRelation'
btnIcon: '@buttonIcon'
},
link: function(scope) {
scope.I18n = I18n;
scope.focusElementIndex = -2;
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');
});
};
}
controller: WorkPackageRelationsController,
controllerAs: '$ctrl',
bindToController: true,
};
}

@ -26,8 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
/* jshint camelcase: false */
module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) {
var WorkPackagesHelper = {
@ -143,72 +141,6 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) {
parseDateTime: function(value) {
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