resolve merge conflict in frontend/app/components/wp-query-menu/wp-query-menu.service.ts

pull/6274/head
Wieland Lindenthal 7 years ago
commit 5535490cc2
  1. 1
      app/assets/stylesheets/layout/_work_package_table_embedded.sass
  2. 15
      app/helpers/application_helper.rb
  3. 61
      app/helpers/meta_tags_helper.rb
  4. 2
      app/policies/work_package_policy.rb
  5. 2
      app/views/layouts/base.html.erb
  6. 2
      app/views/layouts/help.html.erb
  7. 2
      app/views/timelog/index.html.erb
  8. 2
      browserslist
  9. 4
      config/locales/js-en.yml
  10. 2
      docs/installation/system-requirements.md
  11. 23
      frontend/app/angular4-modules.ts
  12. 27
      frontend/app/components/html/op-title.service.ts
  13. 11
      frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts
  14. 4
      frontend/app/components/wp-copy/wp-copy.controller.ts
  15. 36
      frontend/app/components/wp-edit-form/work-package-filter-values.ts
  16. 69
      frontend/app/components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper.ts
  17. 19
      frontend/app/components/wp-inline-create/wp-inline-create.component.ts
  18. 7
      frontend/app/components/wp-new/wp-create.controller.ts
  19. 7
      frontend/app/components/wp-query-menu/wp-query-menu.ng2.test.ts
  20. 29
      frontend/app/components/wp-query-menu/wp-query-menu.service.ts
  21. 26
      frontend/app/components/wp-relations/ng1-wp-relations-wrapper.directive.ts
  22. 3
      frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html
  23. 84
      frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts
  24. 161
      frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html
  25. 27
      frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html
  26. 58
      frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html
  27. 28
      frontend/app/components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive.ts
  28. 64
      frontend/app/components/wp-relations/wp-relations-create/wp-relation-create.template.html
  29. 113
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts
  30. 15
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html
  31. 74
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts
  32. 63
      frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts
  33. 86
      frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts
  34. 48
      frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html
  35. 112
      frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts
  36. 5
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts
  37. 38
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html
  38. 2
      frontend/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html
  39. 71
      frontend/app/components/wp-relations/wp-relations.component.ts
  40. 9
      frontend/app/components/wp-relations/wp-relations.interfaces.ts
  41. 33
      frontend/app/components/wp-relations/wp-relations.service.ts
  42. 42
      frontend/app/components/wp-relations/wp-relations.template.html
  43. 2
      frontend/app/components/wp-single-view-tabs/relations-tab/relations-tab.html
  44. 10
      frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts
  45. 7
      frontend/app/components/wp-table/wp-table-configuration.ts
  46. 6
      frontend/app/globals/unsupported-browsers.ts
  47. 2
      frontend/app/modules/hal/dm-services/relations-dm.service.ts
  48. 12
      frontend/app/modules/hal/resources/work-package-resource.ts
  49. 21
      spec/features/work_packages/details/relations/hierarchy_spec.rb
  50. 13
      spec/support/components/work_packages/relations.rb
  51. 8
      spec/support/pages/work_packages_table.rb

@ -33,6 +33,7 @@
.work-package-table--container
contain: initial !important
overflow: visible
.generic-table--header,
.generic-table--sort-header

@ -349,21 +349,6 @@ module ApplicationHelper
end
end
def html_title(*args)
title = []
if args.empty?
title << h(@project.name) if @project
title += @html_title if @html_title
else
@html_title ||= []
@html_title += args
title += @html_title
end
title.select { |t| !t.blank? }.join(' - ').html_safe
end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes

@ -0,0 +1,61 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
module MetaTagsHelper
##
# Use meta-tags to output title and site name
def output_title_and_meta_tags
display_meta_tags site: Setting.app_title,
title: html_title_parts,
separator: ' | ', # Update the TitleService when changing this!
reverse: true
end
##
# Writer of html_title as string
def html_title(*args)
title = []
raise "Don't use html_title getter" if args.empty?
@html_title ||= []
@html_title += args
end
##
# The html title parts currently defined
def html_title_parts
[].tap do |parts|
parts << h(@project.name) if @project
parts.concat @html_title.map(&:to_s) if @html_title
end
end
end

@ -102,6 +102,8 @@ class WorkPackagePolicy < BasePolicy
end
def type_active_in_project?(work_package)
return false unless work_package.project
@type_active_cache ||= Hash.new do |hash, project|
hash[project] = project.types.pluck(:id)
end

@ -31,7 +31,7 @@ See docs/COPYRIGHT.rdoc for more details.
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width">
<%= display_meta_tags site: Setting.app_title, title: html_title %>
<%= output_title_and_meta_tags %>
<meta name="app_base_path" content="<%= OpenProject::Configuration['rails_relative_url_root'] || '' %>" />
<% if @project %>

@ -29,7 +29,7 @@ See docs/COPYRIGHT.rdoc for more details.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title><%= h html_title %></title>
<%= output_title_and_meta_tags %>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<%= nonced_style_tag do %>
<%= yield(:styles) %>

@ -60,7 +60,7 @@ See docs/COPYRIGHT.rdoc for more details.
<% end %>
<% end %>
</div>
<% html_title l(:label_spent_time), l(:label_details) %>
<% html_title l(:label_spent_time) %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {issue_id: @issue, format: 'atom', key: User.current.rss_key}, title: l(:label_spent_time)) %>
<% end %>

@ -4,4 +4,4 @@
last 2 Chrome versions
last 2 Safari versions
last 2 Edge versions
Firefox >= 45
Firefox >= 60

@ -398,7 +398,6 @@ en:
grouping_other: "Other"
noneSelection: "(none)"
name: "Name"
new_work_package: "New work package"
outline: "Reset Outline"
outlines:
aggregation: "Show aggregations only"
@ -490,10 +489,13 @@ en:
inline_create:
title: 'Click here to add a new work package to this list'
create:
title: 'New work package'
header: 'New %{type}'
header_no_type: 'New work package (Type not yet set)'
header_with_parent: 'New %{type} (Child of %{parent_type} #%{id})'
button: 'Create'
copy:
title: 'Copy work package'
hierarchy:
show: "Show hierarchy mode"
hide: "Hide hierarchy mode"

@ -43,7 +43,7 @@ OpenProject supports the latest versions of the major browsers. In our
strive to make OpenProject easy and fun to use we had to drop support
for some older browsers (e.g. IE 11).
* [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/products/)
* [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/products/) (At least ESR version 60)
* [Microsoft Edge](https://www.microsoft.com/de-de/windows/microsoft-edge)
* [Google Chrome](https://www.google.com/chrome/browser/desktop/)

@ -48,7 +48,7 @@ import {WorkPackageTableRelationColumnsService} from 'core-components/wp-fast-ta
import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp-table-selection.service';
import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state/wp-table-sort-by.service';
import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service';
import {WorkPackageInlineCreateComponent,} from 'core-components/wp-inline-create/wp-inline-create.component';
import {WorkPackageInlineCreateComponent} from 'core-components/wp-inline-create/wp-inline-create.component';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {WpResizerDirective} from 'core-components/resizer/wp-resizer.component';
import {MainMenuResizerDirective} from 'core-components/resizer/main-menu-resizer.component';
@ -74,7 +74,8 @@ import {
I18nToken,
TextileServiceToken,
upgradeService,
upgradeServiceWithToken, WorkPackageServiceToken,
upgradeServiceWithToken,
WorkPackageServiceToken,
wpMoreMenuServiceToken
} from './angular4-transition-utils';
import {WpCustomActionComponent} from 'core-components/wp-custom-actions/wp-custom-actions/wp-custom-action.component';
@ -130,7 +131,6 @@ import {WorkPackagesFullViewComponent} from 'core-components/routing/wp-full-vie
import {WorkPackageActivityTabComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-tab.component';
import {WorkPackageRelationsTabComponent} from 'core-components/wp-single-view-tabs/relations-tab/relations-tab.component';
import {WorkPackageWatchersTabComponent} from 'core-components/wp-single-view-tabs/watchers-tab/watchers-tab.component';
import {Ng1RelationsDirectiveWrapper} from 'core-components/wp-relations/ng1-wp-relations-wrapper.directive';
import {WorkPackageWatcherEntryComponent} from 'core-components/wp-single-view-tabs/watchers-tab/wp-watcher-entry.component';
import {WorkPackageNewFullViewComponent} from 'core-components/wp-new/wp-new-full-view.component';
import {WorkPackageTypeStatusComponent} from 'core-components/work-packages/wp-type-status/wp-type-status.component';
@ -165,7 +165,6 @@ import {WorkPackageEditFieldService} from 'core-components/wp-edit/wp-edit-field
import {WorkPackageEmbeddedTableComponent} from 'core-components/wp-table/embedded/wp-embedded-table.component';
import {OpTableActionsService} from 'core-components/wp-table/table-actions/table-actions.service';
import {WorkPackageRelationsHierarchyComponent} from 'core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive';
import {Ng1RelationsCreateWrapper} from 'core-components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive';
import {WpRelationsAutocompleteComponent} from 'core-components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component';
import {WpRelationAddChildComponent} from 'core-components/wp-relations/wp-relation-add-child/wp-relation-add-child';
import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relations-parent/wp-relations-parent.component';
@ -221,6 +220,12 @@ import {WpDestroyModal} from "core-components/modals/wp-destroy-modal/wp-destroy
import {FocusWithinDirective} from "core-components/common/focus/focus-within.upgraded.directive";
import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive";
import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp-relation-children/wp-children-query.component';
import {OpTitleService} from 'core-components/html/op-title.service';
import {WorkPackageRelationsComponent} from "core-components/wp-relations/wp-relations.component";
import {WorkPackageRelationsGroupComponent,} from "core-components/wp-relations/wp-relations-group/wp-relations-group.component";
import {WorkPackageRelationRowComponent} from "core-components/wp-relations/wp-relation-row/wp-relation-row.component";
import {Ng1FieldControlsWrapper} from "core-components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper";
import {WorkPackageRelationsCreateComponent} from "core-components/wp-relations/wp-relations-create/wp-relations-create.component";
@NgModule({
imports: [
@ -250,8 +255,9 @@ import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp
FocusHelperService,
PathHelperService,
upgradeServiceWithToken('wpMoreMenuService', wpMoreMenuServiceToken),
OpTitleService,
TimezoneService,
upgradeService('wpRelations', WorkPackageRelationsService),
WorkPackageRelationsService,
UrlParamsHelperService,
WorkPackageCacheService,
WorkPackageEditingService,
@ -398,8 +404,11 @@ import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp
// Relations Tab
WorkPackageRelationsTabComponent,
Ng1RelationsDirectiveWrapper,
Ng1RelationsCreateWrapper,
WorkPackageRelationsComponent,
WorkPackageRelationsGroupComponent,
WorkPackageRelationRowComponent,
WorkPackageRelationsCreateComponent,
Ng1FieldControlsWrapper,
WorkPackageRelationsHierarchyComponent,
WpRelationsAutocompleteComponent,
WpRelationAddChildComponent,

@ -0,0 +1,27 @@
import {Title} from "@angular/platform-browser";
import {Injectable} from "@angular/core";
const titlePartsSeparator = ' | ';
@Injectable()
export class OpTitleService {
constructor(private titleService:Title) {
}
public get current():string {
return this.titleService.getTitle();
}
public get titleParts():string[] {
return this.current.split(titlePartsSeparator);
}
public setFirstPart(value:string) {
let parts = this.titleParts;
parts[0] = value;
this.titleService.setTitle(parts.join(titlePartsSeparator));
}
}

@ -39,6 +39,8 @@ import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.servic
import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-request.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {ProjectCacheService} from 'core-components/projects/project-cache.service';
import {OpTitleService} from 'core-components/html/op-title.service';
import {AuthorisationService} from "core-components/common/model-auth/model-auth.service";
export class WorkPackageViewController implements OnDestroy {
@ -51,6 +53,7 @@ export class WorkPackageViewController implements OnDestroy {
protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
protected wpTableFocus:WorkPackageTableFocusService = this.injector.get(WorkPackageTableFocusService);
protected projectCacheService:ProjectCacheService = this.injector.get(ProjectCacheService);
protected authorisationService:AuthorisationService = this.injector.get(AuthorisationService);
// Static texts
public text:any = {};
@ -62,6 +65,8 @@ export class WorkPackageViewController implements OnDestroy {
protected focusAnchorLabel:string;
public showStaticPagePath:string;
readonly titleService:OpTitleService = this.injector.get(OpTitleService);
constructor(public injector:Injector, protected workPackageId:string) {
this.initializeTexts();
}
@ -106,6 +111,12 @@ export class WorkPackageViewController implements OnDestroy {
this.projectIdentifier = this.workPackage.project.identifier;
});
// Set authorisation data
this.authorisationService.initModelAuth('work_package', this.workPackage.$links);
// Push the current title
this.titleService.setFirstPart(this.workPackage.subjectWithType(20));
// Preselect this work package for future list operations
this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackageId);

@ -45,6 +45,10 @@ export class WorkPackageCopyController extends WorkPackageCreateController {
});
}
protected setTitle() {
this.titleService.setFirstPart(this.I18n.t('js.work_packages.copy.title'));
}
private async createCopyFrom(wp:WorkPackageResource) {
const changeset = this.wpEditing.changesetFor(wp);
return changeset.getForm().then(async (form:any) => {

@ -3,6 +3,7 @@ import {CollectionResource} from 'core-app/modules/hal/resources/collection-reso
import {FormResource} from 'core-app/modules/hal/resources/form-resource';
import {WorkPackageChangeset} from './work-package-changeset';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
import {all} from "@uirouter/core";
export class WorkPackageFilterValues {
@ -43,16 +44,7 @@ export class WorkPackageFilterValues {
private async setAllowedValueFor(form:FormResource, field:string, value:string|HalResource) {
return this.allowedValuesFor(form, field).then((allowedValues) => {
let newValue;
if ((value as HalResource)['$href']) {
newValue = _.find(allowedValues,
(entry:any) => entry.$href === (value as HalResource).$href);
} else if (allowedValues) {
newValue = _.find(allowedValues, (entry:any) => entry === value);
} else {
newValue = value;
}
let newValue = this.findSpecialValue(value, field) || this.findAllowedValue(value, allowedValues);
if (newValue) {
this.changeset.setValue(field, newValue);
@ -61,6 +53,30 @@ export class WorkPackageFilterValues {
});
}
/**
* Returns special values for which no allowed values exist (e.g., parent ID in embedded queries)
* @param {string | HalResource} value
* @param {string} field
*/
private findSpecialValue(value:string|HalResource, field:string):string|HalResource|undefined {
if (field === 'parent') {
return value;
}
return undefined;
}
private findAllowedValue(value:string|HalResource, allowedValues:HalResource[]) {
if (value instanceof HalResource && !!value.$href) {
return _.find(allowedValues,
(entry:any) => entry.$href === value.$href);
} else if (allowedValues) {
return _.find(allowedValues, (entry:any) => entry === value);
} else {
return value;
}
}
private async allowedValuesFor(form:FormResource, field:string):Promise<HalResource[]> {
const fieldSchema = form.schema[field];

@ -0,0 +1,69 @@
//-- 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.
//++
// This Angular directive will act as an interface to the "upgraded" AngularJS component
import {
Directive,
DoCheck,
ElementRef,
EventEmitter,
Inject,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
@Directive({selector: 'ng1-wp-field-controls-wrapper'})
export class Ng1FieldControlsWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@Input() public fieldController:any;
@Input() public cancelTitle:string;
@Input() public saveTitle:string;
@Output() public onSave = new EventEmitter<undefined>();
@Output() public onCancel = new EventEmitter<undefined>();
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) {
// We must pass the name of the directive as used by AngularJS to the super
super('wpEditFieldControls', elementRef, injector);
}
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
// because the AoT compiler will not realise that the super class implements them
ngOnInit() { super.ngOnInit(); }
ngOnChanges(changes:SimpleChanges) { super.ngOnChanges(changes); }
ngDoCheck() { super.ngDoCheck(); }
ngOnDestroy() { super.ngOnDestroy(); }
}

@ -28,7 +28,7 @@
import {
Component,
ElementRef,
ElementRef, HostListener,
Inject,
Injector,
Input,
@ -122,7 +122,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
this.timelineBuilder = new TimelineRowBuilder(this.injector, this.table);
// Mirror the row height in timeline
const container = jQuery('.wp-table-timeline--body');
const container = jQuery(this.table.timelineBody);
container.addClass('-inline-create-mirror');
// Remove temporary rows on creation of new work package
@ -137,7 +137,9 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
this.addWorkPackageRow();
// Focus on the last inserted id
this.wpTableFocus.updateFocus(wp.id);
if (!this.table.configuration.isEmbedded) {
this.wpTableFocus.updateFocus(wp.id);
}
} else {
// Remove current row
this.table.editing.stopEditing('new');
@ -171,11 +173,6 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
evt.stopImmediatePropagation();
return false;
});
// Additionally, cancel on escape
Mousetrap(this.$element[0]).bind('escape', () => {
this.resetRow();
});
}
public handleAddRowClick() {
@ -192,7 +189,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
const wp = this.currentWorkPackage = changeset.workPackage;
// Apply filter values
const filter = new WorkPackageFilterValues(changeset, this.wpTableFilters.current);
const filter = new WorkPackageFilterValues(changeset, this.tableState.query.value!.filters);
filter.applyDefaultsFromFilters().then(() => {
this.wpEditing.updateValue('new', changeset);
this.wpCacheService.updateWorkPackage(this.currentWorkPackage!);
@ -222,6 +219,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
/**
* Reset the new work package row and refocus on the button
*/
@HostListener('keydown.escape')
public resetRow() {
this.focus = true;
this.removeWorkPackageRow();
@ -251,6 +249,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
}
public get isAllowed():boolean {
return this.authorisationService.can('work_packages', 'createWorkPackage');
return this.authorisationService.can('work_packages', 'createWorkPackage') ||
this.authorisationService.can('work_package', 'addChild');
}
}

@ -43,6 +43,7 @@ import {WorkPackageTableFiltersService} from '../wp-fast-table/state/wp-table-fi
import {WorkPackageCreateService} from './wp-create.service';
import {takeUntil} from 'rxjs/operators';
import {RootDmService} from 'core-app/modules/hal/dm-services/root-dm.service';
import {OpTitleService} from 'core-components/html/op-title.service';
export class WorkPackageCreateController implements OnInit, OnDestroy {
@ -59,6 +60,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
constructor(readonly $transition:Transition,
@Inject($stateToken) readonly $state:StateService,
@Inject(I18nToken) readonly I18n:op.I18n,
readonly titleService:OpTitleService,
protected wpNotificationsService:WorkPackageNotificationService,
protected states:States,
protected wpCreate:WorkPackageCreateService,
@ -76,6 +78,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
this.changeset = changeset;
this.newWorkPackage = changeset.workPackage;
this.setTitle();
this.wpCacheService.updateWorkPackage(this.newWorkPackage);
this.wpEditing.updateValue('new', changeset);
@ -122,6 +125,10 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
this.$state.go('work-packages.new', this.$state.params);
}
protected setTitle() {
this.titleService.setFirstPart(this.I18n.t('js.work_packages.create.title'));
}
protected async newWorkPackageFromParams(stateParams:any):Promise<WorkPackageChangeset> {
const type = parseInt(stateParams.type);

@ -34,6 +34,8 @@ import {Component} from '@angular/core';
import {WorkPackagesListChecksumService} from 'core-components/wp-list/wp-list-checksum.service';
import {TransitionService} from '@uirouter/core';
import {$stateToken} from 'core-app/angular4-transition-utils';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {OpTitleService} from 'core-components/html/op-title.service';
@Component({
template: `
@ -57,6 +59,9 @@ describe('wp-query-menu', () => {
const $transitionStub = {
onStart: (criteria:any, callback:(transition:any) => any) => {
transitionCallback = (id:any) => callback({
to: () => {
return { name: 'asdf' };
},
params: (val:string) => { return { query_id: id }; }
} as any);
}
@ -70,6 +75,8 @@ describe('wp-query-menu', () => {
WpQueryMenuTestComponent
],
providers: [
{ provide: I18nToken, useValue: I18n },
{ provide: OpTitleService, useValue: { setFirstPart: () => { return; } } },
{ provide: $stateToken, useValue: { params: { query_id: null }, go: (...args:any[]) => undefined } },
{ provide: WorkPackagesListChecksumService, useValue: { clear: () => undefined } },
{ provide: TransitionService, useValue: $transitionStub },

@ -28,9 +28,11 @@
import {Inject, Injectable} from '@angular/core';
import {StateService, Transition, TransitionService} from '@uirouter/core';
import {$stateToken} from 'core-app/angular4-transition-utils';
import {$stateToken, I18nToken} from 'core-app/angular4-transition-utils';
import {LinkHandling} from 'core-components/common/link-handling/link-handling';
import {WorkPackagesListChecksumService} from 'core-components/wp-list/wp-list-checksum.service';
import {Title} from '@angular/platform-browser';
import {OpTitleService} from 'core-components/html/op-title.service';
export const QUERY_MENU_ITEM_TYPE = 'query-menu-item';
@ -48,13 +50,23 @@ export class QueryMenuService {
private uiRouteStateName = 'work-packages.list';
private container:JQuery;
constructor(@Inject($stateToken) protected $state:StateService,
protected $transitions:TransitionService,
protected wpListChecksumService:WorkPackagesListChecksumService) {
constructor(@Inject($stateToken) readonly $state:StateService,
@Inject(I18nToken) readonly I18n:op.I18n,
readonly titleService:OpTitleService,
readonly $transitions:TransitionService,
readonly wpListChecksumService:WorkPackagesListChecksumService) {
this.$transitions.onStart({}, (transition:Transition) => {
const queryId = transition.params('to').query_id;
this.onQueryIdChanged(queryId);
// Update query menu and title when either
// the query menu id changed
const queryIdChanged = this.currentQueryId !== queryId;
// we're moving to the work-packges.list state
const movingToWPList = transition.to().name === 'work-packages.list';
if (movingToWPList || queryIdChanged) {
this.onQueryIdChanged(queryId);
}
});
this.initialize();
@ -117,11 +129,16 @@ export class QueryMenuService {
// Update all queries children
const queries = this.container.find('.query-menu-item');
queries.toggleClass('selected', false);
if (this.currentQueryId) {
// Unselect default query if a current query id is set
this.container.find('.all-open-wps-menu-item').removeClass('selected');
// Set active query to selected.
queries.filter(`#wp-query-menu-item-${this.currentQueryId}`).addClass('selected');
let current = queries.filter(`#wp-query-menu-item-${this.currentQueryId}`)
current.addClass('selected');
// Set the page title
this.titleService.setFirstPart(current.text());
}
}

@ -1,26 +0,0 @@
// This Angular directive will act as an interface to the "upgraded" AngularJS component
// query-filters
import {
Directive,
DoCheck,
ElementRef,
Inject,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit
} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
@Directive({selector: 'ng1-wp-relations-wrapper'})
export class Ng1RelationsDirectiveWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@Input('workPackage') workPackage:WorkPackageResource;
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) {
// We must pass the name of the directive as used by AngularJS to the super
super('wpRelations', elementRef, injector);
}
}

@ -4,9 +4,10 @@
[tableActions]="childrenTableActions"
[configuration]="{ actionsColumnEnabled: true,
hierarchyToggleEnabled: false,
inlineCreateEnabled: false,
inlineCreateEnabled: true,
columnMenuEnabled: false,
contextMenuEnabled: false,
projectIdentifier: workPackage.project.idFromLink,
projectContext: false }" >
</wp-embedded-table>

@ -1,17 +1,24 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageRelationsService} from '../wp-relations.service';
import {keyCodes} from 'core-components/common/keyCodes.enum';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from "@angular/core";
import {I18nToken} from "core-app/angular4-transition-utils";
@Component({
selector: 'wp-relation-row',
template: require('!!raw-loader!./wp-relation-row.template.html')
})
export class WorkPackageRelationRowComponent implements OnInit {
@Input() public workPackage:WorkPackageResource;
@Input() public relatedWorkPackage:WorkPackageResource;
@Input() public groupByWorkPackageType:boolean;
@ViewChild('relationDescriptionTextarea') readonly relationDescriptionTextarea:ElementRef;
class WpRelationRowDirectiveController {
public workPackage:WorkPackageResource;
public relatedWorkPackage:WorkPackageResource;
public relationType:string;
public groupByWorkPackageType:boolean;
public showRelationInfo:boolean = false;
public showEditForm:boolean = false;
public availableRelationTypes:{ name:string }[];
@ -33,36 +40,33 @@ class WpRelationRowDirectiveController {
};
public relation:RelationResource;
public text:Object;
public text = {
cancel: this.I18n.t('js.button_cancel'),
save: this.I18n.t('js.button_save'),
removeButton: this.I18n.t('js.relation_buttons.remove'),
description_label: this.I18n.t('js.relation_buttons.update_description'),
toggleDescription: this.I18n.t('js.relation_buttons.toggle_description'),
updateRelation: this.I18n.t('js.relation_buttons.update_relation'),
placeholder: {
description: this.I18n.t('js.placeholders.relation_description')
}
};
constructor(protected $scope:ng.IScope,
protected $element:ng.IAugmentedJQuery,
protected $timeout:ng.ITimeoutService,
protected $http:ng.IHttpService,
protected wpCacheService:WorkPackageCacheService,
constructor(protected wpCacheService:WorkPackageCacheService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpRelations:WorkPackageRelationsService,
protected I18n:op.I18n,
@Inject(I18nToken) protected I18n:op.I18n,
protected PathHelper:PathHelperService) {
}
ngOnInit() {
this.relation = this.relatedWorkPackage.relatedBy as RelationResource;
this.text = {
cancel: I18n.t('js.button_cancel'),
save: I18n.t('js.button_save'),
removeButton: I18n.t('js.relation_buttons.remove'),
description_label: I18n.t('js.relation_buttons.update_description'),
toggleDescription: I18n.t('js.relation_buttons.toggle_description'),
updateRelation: I18n.t('js.relation_buttons.update_relation'),
placeholder: {
description: I18n.t('js.placeholders.relation_description')
}
};
this.userInputs.newRelationText = this.relation.description || '';
this.availableRelationTypes = RelationResource.LOCALIZED_RELATION_TYPES(false);
this.selectedRelationType = _.find(this.availableRelationTypes,
{'name': this.relation.normalizedType(this.workPackage)}) as RelationResource;
};
}
/**
* Return the normalized relation type for the work package we're viewing.
@ -79,9 +83,9 @@ class WpRelationRowDirectiveController {
public startDescriptionEdit() {
this.userInputs.showDescriptionEditForm = true;
this.$timeout(() => {
var textarea = this.$element.find('.wp-relation--description-textarea');
var textlen = textarea.val().length;
setTimeout(() => {
const textarea = jQuery(this.relationDescriptionTextarea.nativeElement);
const textlen = textarea.val().length;
// Focus and set cursor to end
textarea.focus();
@ -124,9 +128,7 @@ class WpRelationRowDirectiveController {
}
public cancelRelationTypeEditOnEscape(evt:JQueryEventObject) {
if (evt.which === keyCodes.ESCAPE) {
this.userInputs.showRelationTypesForm = false;
}
this.userInputs.showRelationTypesForm = false;
}
public saveRelationType() {
@ -154,28 +156,8 @@ class WpRelationRowDirectiveController {
.then(() => {
this.wpCacheService.updateWorkPackage(this.relatedWorkPackage);
this.wpNotificationsService.showSave(this.relatedWorkPackage);
this.$timeout(() => {
angular.element('#relation--add-relation').focus();
});
})
.catch((err:any) => this.wpNotificationsService.handleErrorResponse(err,
this.relatedWorkPackage));
}
}
function WpRelationRowDirective($timeout:ng.ITimeoutService):any {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relation-row/wp-relation-row.template.html',
scope: {
workPackage: '=',
groupByWorkPackageType: '=',
relatedWorkPackage: '='
},
controller: WpRelationRowDirectiveController,
controllerAs: '$ctrl',
bindToController: true
};
}
wpDirectivesModule.directive('wpRelationRow', WpRelationRowDirective);

@ -1,95 +1,96 @@
<div class="relation-row"
ng-class="['relation-row-{{ $ctrl.relatedWorkPackage.id }}']"
<div *ngIf="workPackage && relatedWorkPackage"
class="relation-row relation-row-{{ relatedWorkPackage.id }}"
focus-within="'.wp-relations-controls-section accessible-by-keyboard'">
<div class="grid-block hierarchy-item">
<div class="grid-block hierarchy-item">
<div class="grid-content medium-3 collapse">
<div class="grid-content medium-3 collapse">
<span class="relation-row--type"
ng-click="$ctrl.activateRelationTypeEdit()"
(accessibleClick)="activateRelationTypeEdit()"
tabindex="0"
ng-if="!$ctrl.userInputs.showRelationTypesForm">
*ngIf="!userInputs.showRelationTypesForm">
<span ng-if="$ctrl.groupByWorkPackageType"
ng-bind="$ctrl.normalizedRelationType"></span>
<span ng-if="!$ctrl.groupByWorkPackageType"
ng-bind="$ctrl.relatedWorkPackage.type.name"></span>
<span class="hidden-for-sighted" ng-bind="::$ctrl.text.updateRelation"></span>
<span *ngIf="groupByWorkPackageType"
[textContent]="normalizedRelationType"></span>
<span *ngIf="!groupByWorkPackageType"
[textContent]="relatedWorkPackage.type.name"></span>
<span class="hidden-for-sighted" [textContent]="text.updateRelation"></span>
</span>
<div class="wp-edit-field inplace-edit"
ng-if="$ctrl.userInputs.showRelationTypesForm">
<select class="wp-inline-edit--field form--select"
ng-model="$ctrl.selectedRelationType"
role="listbox"
focus
ng-keydown="$ctrl.cancelRelationTypeEditOnEscape($event)"
ng-options="relationType as relationType.label for relationType in $ctrl.availableRelationTypes track by relationType.name"
ng-change="$ctrl.saveRelationType()"></select>
</div>
</div>
<div class="wp-edit-field inplace-edit"
*ngIf="userInputs.showRelationTypesForm">
<select class="wp-inline-edit--field form--select"
[(ngModel)]="selectedRelationType"
(change)="saveRelationType()"
role="listbox"
focus
(keydown.escape)="cancelRelationTypeEditOnEscape($event)">
<option *ngFor="let relationType of availableRelationTypes"
[textContent]="relationType.label"
[ngValue]="relationType"></option>
</select>
</div>
</div>
<div class="grid-content medium-5 collapse" wp-single-relation
ng-if="$ctrl.relatedWorkPackage">
<a ui-sref="work-packages.show.relations({ workPackageId: $ctrl.relatedWorkPackage.id})"
class="wp-relations--subject-field"
aria-label="{{ $ctrl.normalizedRelationType + ' ' + singleRelationCtrl.getFullIdentifier($ctrl.relatedWorkPackage, true) }}">
{{ singleRelationCtrl.getFullIdentifier($ctrl.relatedWorkPackage, true) }}
</a>
</div>
<div class="grid-content medium-5 collapse"
*ngIf="relatedWorkPackage">
<a uiSref="work-packages.show.relations"
[uiParams]="{ workPackageId: relatedWorkPackage.id }"
class="wp-relations--subject-field"
[textContent]="relatedWorkPackage.subjectWithType()"
[attr.aria-label]="normalizedRelationType + ' ' + relatedWorkPackage.subjectWithType()">
</a>
</div>
<div class="grid-content medium-2 collapse wp-relations-status-field">
<wp-edit-field-group-ng1 work-package="$ctrl.relatedWorkPackage">
<div wp-edit-form="$ctrl.relatedWorkPackage" ng-if="$ctrl.relatedWorkPackage">
<wp-edit-field work-package-id="$ctrl.relatedWorkPackage.id"
field-name="'status'">
</wp-edit-field>
</div>
</wp-edit-field-group-ng1>
</div>
<div class="grid-content medium-2 collapse wp-relations-status-field">
<wp-edit-field-group *ngIf="relatedWorkPackage" [workPackage]="relatedWorkPackage">
<wp-edit-field [workPackageId]="relatedWorkPackage.id" fieldName="status"></wp-edit-field>
</wp-edit-field-group>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section"
ng-class="{'-expanded': $ctrl.userInputs.showRelationInfo }">
<accessible-by-keyboard link-aria-label="{{ ::$ctrl.text.description_label }}"
link-title="{{ ::$ctrl.text.description_label }}"
link-class="wp-relations--description-btn"
ng-class="{'-visible': $ctrl.showDescriptionInfo }"
execute="$ctrl.userInputs.showRelationInfo = !$ctrl.userInputs.showRelationInfo">
<op-icon icon-classes="icon-info1 wp-relations--icon wp-relations--description-icon" icon-title="{{ ::$ctrl.text.toggleDescription }}"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard ng-if="$ctrl.relation.delete"
execute="$ctrl.removeRelation($ctrl.relation)"
aria-hidden="false"
link-aria-label="{{ ::$ctrl.text.remove }}"
link-title="{{ ::$ctrl.text.remove }}"
link-class="relation-row--remove-btn">
<op-icon icon-classes="icon-remove wp-relations--icon" icon-title="{{ ::$ctrl.text.removeButton }}"></op-icon>
</accessible-by-keyboard>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section"
ng-class="{'-expanded': userInputs.showRelationInfo }">
<accessible-by-keyboard [linkAriaLabel]="text.description_label"
[linkTitle]="text.description_label"
linkClass="wp-relations--description-btn"
[ngClass]="{'-visible': showDescriptionInfo }"
(execute)="userInputs.showRelationInfo = !userInputs.showRelationInfo">
<op-icon icon-classes="icon-info1 wp-relations--icon wp-relations--description-icon"
icon-title="text.toggleDescription"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard *ngIf="relation.delete"
(execute)="removeRelation(relation)"
aria-hidden="false"
[linkAriaLabel]="text.remove"
[linkTitle]="text.remove"
linkClass="relation-row--remove-btn">
<op-icon icon-classes="icon-remove wp-relations--icon" [icon-title]="text.removeButton"></op-icon>
</accessible-by-keyboard>
</div>
</div>
<div class="grid-block hierarchy-item description-container"
ng-show="$ctrl.userInputs.showRelationInfo">
<div class="wp-relation--description-read-value"
ng-class="{'-placeholder': !$ctrl.relation.description }"
ng-click="$ctrl.startDescriptionEdit()"
ng-hide="$ctrl.userInputs.showDescriptionEditForm"
ng-bind="$ctrl.relation.description || $ctrl.text.placeholder.description">
</div>
<div class="wp-relation--description-wrapper textarea-wrapper"
ng-show="$ctrl.userInputs.showDescriptionEditForm">
<div class="grid-block hierarchy-item description-container"
*ngIf="userInputs.showRelationInfo">
<div class="wp-relation--description-read-value"
[ngClass]="{'-placeholder': !relation.description }"
(accessibleClick)="startDescriptionEdit()"
*ngIf="!userInputs.showDescriptionEditForm"
[textContent]="relation.description || text.placeholder.description">
</div>
<div class="wp-relation--description-wrapper textarea-wrapper"
*ngIf="userInputs.showDescriptionEditForm">
<textarea
msd-elastic="\n"
autofocus
class="wp-relation--description-textarea"
name="description"
ng-keyup="$ctrl.handleDescriptionKey($event)"
ng-model="$ctrl.userInputs.newRelationText"></textarea>
<wp-edit-field-controls field-controller="$ctrl.fieldController"
on-save="$ctrl.saveDescription()"
on-cancel="$ctrl.cancelDescriptionEdit()"
save-title="{{ vm.field.text.save }}"
cancel-title="{{ vm.field.text.cancel }}">
</wp-edit-field-controls>
</div>
#relationDescriptionTextarea
autofocus
class="wp-relation--description-textarea"
name="description"
(keyup)="handleDescriptionKey($event)"
[(ngModel)]="userInputs.newRelationText"></textarea>
<ng1-wp-field-controls-wrapper [fieldController]="fieldController"
(onSave)="saveDescription()"
(onCancel)="cancelDescriptionEdit()"
[saveTitle]="text.save"
[cancelTitle]="text.cancel">
</ng1-wp-field-controls-wrapper>
</div>
</div>
</div>

@ -1,27 +0,0 @@
<div class="loading-indicator--location"
data-indicator-name="relationAddFixed">
<div class="grid-block v-align collapse wp-relations-create"
ng-if="$ctrl.showRelationsCreateForm || $ctrl.externalFormToggle">
<div class="grid-content medium-10 collapse">
<wp-relations-autocomplete
selected-wp-id="$ctrl.selectedWpId"
loading-promise-name="relationAddFixed"
selected-relation-type="$ctrl.selectedRelationType"
focus>
</wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$ctrl.createRelation()"
is-disabled="$ctrl.isDisabled"
aria-hidden="false">
<op-icon icon-classes="icon-checkmark" icon-title="{{ ::$ctrl.text.save }}"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$ctrl.toggleRelationsCreateForm()"
aria-hidden="false">
<op-icon icon-classes="icon-remove" icon-title="{{ ::$ctrl.text.abort }}"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -1,58 +0,0 @@
<div class="wp-relations-create">
<div class="wp-relations-create-button hide-when-print"
ng-if="!$ctrl.showRelationsCreateForm">
<div class="grid-block">
<div class="grid-content collapse wp-inline-create-button">
<a class="wp-inline-create--add-link relation-create -focus-after-save"
ng-click="$ctrl.toggleRelationsCreateForm()"
href
id="relation--add-relation">
<op-icon icon-classes="icon icon-add"></op-icon>
<span>{{ ::$ctrl.text.addNewRelation }}</span>
</a>
</div>
</div>
</div>
<div class="loading-indicator--location"
data-indicator-name="relationAddDynamic">
<div class="v-align wp-relations-create--form" ng-if="$ctrl.showRelationsCreateForm">
<div class="grid-content collapse medium-3">
<label class="hidden-for-sighted" for="relation-type--select">{{ ::$ctrl.text.relationType }}</label>
<select class="form--select relationTypeSelect"
id="relation-type--select"
role="listbox"
ng-model="$ctrl.selectedRelationType"
ng-options="type.name as type.label for type in $ctrl.relationTypes"
focus>
</select>
</div>
<div class="grid-content medium-7">
<wp-relations-autocomplete
work-package="$ctrl.workPackage"
loading-promise-name="relationAddDynamic"
selected-wp-id="$ctrl.selectedWpId"
selected-relation-type="$ctrl.selectedRelationType">
</wp-relations-autocomplete>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
execute="$ctrl.createRelation()"
is-disabled="$ctrl.isDisabled || !$ctrl.selectedWpId"
link-class="wp-create-relation--save"
aria-hidden="false">
<op-icon icon-classes="icon-checkmark" icon-title="{{ ::$ctrl.text.save }}"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard
execute="$ctrl.toggleRelationsCreateForm()"
link-class="wp-create-relation--cancel"
aria-hidden="false">
<op-icon icon-classes="icon-remove" icon-title="{{ ::$ctrl.text.abort }}"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>
</div>

@ -1,28 +0,0 @@
// This Angular directive will act as an interface to the "upgraded" AngularJS component
// query-filters
import {
Directive,
DoCheck,
ElementRef,
Inject,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit
} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
@Directive({selector: 'ng1-wp-relations-create'})
export class Ng1RelationsCreateWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@Input('workPackage') workPackage:WorkPackageResource;
@Input('fixedRelationType') fixedRelationType:string;
@Input('externalFormToggle') externalFormToggle:boolean;
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) {
// We must pass the name of the directive as used by AngularJS to the super
super('wpRelationsCreate', elementRef, injector);
}
}

@ -0,0 +1,64 @@
<div class="wp-relations-create">
<div class="wp-relations-create-button hide-when-print"
*ngIf="!showRelationsCreateForm">
<div class="grid-block">
<div class="grid-content collapse wp-inline-create-button">
<a class="wp-inline-create--add-link relation-create"
#focusAfterSave
(accessibleClick)="toggleRelationsCreateForm()"
href
id="relation--add-relation">
<op-icon icon-classes="icon icon-add"></op-icon>
<span [textContent]="text.addNewRelation"></span>
</a>
</div>
</div>
</div>
<div class="loading-indicator--location"
data-indicator-name="relationAddDynamic">
<div class="v-align wp-relations-create--form"
*ngIf="showRelationsCreateForm">
<div class="grid-content collapse medium-3">
<label class="hidden-for-sighted"
for="relation-type--select"
[textContent]="text.relationType"></label>
<select class="form--select relationTypeSelect"
id="relation-type--select"
role="listbox"
[(ngModel)]="selectedRelationType"
focus>
<option *ngFor="let type of relationTypes"
[textContent]="type.label"
[value]="type.name"></option>
</select>
</div>
<div class="grid-content medium-7">
<wp-relations-autocomplete-upgraded
[workPackage]="workPackage"
(onWorkPackageIdSelected)="updateSelectedId($event)"
[selectedRelationType]="parent"
loadingPromiseName="relationAddDynamic">
</wp-relations-autocomplete-upgraded>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
(execute)="createRelation()"
[isDisabled]="isDisabled || !selectedWpId"
linkClass="wp-create-relation--save"
aria-hidden="false">
<op-icon icon-classes="icon-checkmark" [icon-title]="text.save"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard
(execute)="toggleRelationsCreateForm()"
linkClass="wp-create-relation--cancel"
aria-hidden="false">
<op-icon icon-classes="icon-remove" [icon-title]="text.abort"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>
</div>

@ -1,113 +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 {wpDirectivesModule} from '../../../../angular-modules';
import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource';
import {LoadingIndicatorService} from '../../../common/loading-indicator/loading-indicator.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
function wpRelationsAutocompleteDirective(
$q:ng.IQService,
PathHelper:any,
$http:ng.IHttpService,
loadingIndicator:LoadingIndicatorService,
I18n:op.I18n) {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html',
scope: {
selectedWpId: '=',
loadingPromiseName: '@',
selectedRelationType: '=',
filterCandidatesFor: '@',
workPackage: '='
},
link: function (scope:any, element:ng.IAugmentedJQuery, attrs:ng.IAttributes) {
scope.text = {
placeholder: I18n.t('js.relations_autocomplete.placeholder')
};
scope.options = [];
scope.relatedWps = [];
let input = jQuery('.wp-relations--autocomplete');
let selected = false;
input.autocomplete({
delay: 250,
autoFocus: false, // Accessibility!
appendTo: '.detail-panel--autocomplete-target',
source: (request:{ term:string }, response:Function) => {
autocompleteWorkPackages(request.term).then((values) => {
selected = false;
response(values.map(wp => {
return { workPackage: wp, value: getIdentifier(wp) };
}));
});
},
select: (evt, ui:any) => {
scope.$evalAsync(() => {
selected = true;
scope.selectedWpId = ui.item.workPackage.id;
});
},
minLength: 0
}).focus(() => !selected && input.autocomplete('search', input.val()));
function getIdentifier(workPackage:WorkPackageResource):string {
if (workPackage) {
return `#${workPackage.id} - ${workPackage.subject}`;
} else {
return '';
}
}
async function autocompleteWorkPackages(query:string):Promise<WorkPackageResource[]> {
element.find('.ui-autocomplete--loading').show();
return scope.workPackage.available_relation_candidates.$link.$fetch({
query: query,
type: scope.filterCandidatesFor || scope.selectedRelationType
}).then((collection:CollectionResource) => {
scope.noResults = collection.count === 0;
element.find('.ui-autocomplete--loading').hide();
return collection.elements || [];
}).catch(() => {
element.find('.ui-autocomplete--loading').hide();
return [];
});
};
scope.$watch('noResults', (noResults:boolean) => {
if (noResults) {
scope.selectedWpId = null;
}
});
}
};
}
wpDirectivesModule.directive('wpRelationsAutocomplete', wpRelationsAutocompleteDirective);

@ -1,15 +0,0 @@
<form name="add_relation_form" class="form">
<input type="text"
class="wp-relations--autocomplete ui-autocomplete--input"
ng-class="{ '-error': noResults }"
placeholder="{{ ::text.placeholder }}">
<div class="ui-autocomplete--loading" style="display: none">
<div class="loading-indicator -small">
<div class="block-1"></div>
<div class="block-2"></div>
<div class="block-3"></div>
<div class="block-4"></div>
<div class="block-5"></div>
</div>
</div>
</form>

@ -1,50 +1,39 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {WorkPackageRelationsHierarchyService} from '../wp-relations-hierarchy/wp-relations-hierarchy.service';
import {WorkPackageRelationsService} from '../wp-relations.service';
import {Component, ElementRef, Inject, Input, ViewChild} from "@angular/core";
import {I18nToken} from "../../../angular4-transition-utils";
export class WorkPackageRelationsCreateController {
@Component({
selector: 'wp-relations-create',
template: require('!!raw-loader!./wp-relation-create.template.html')
})
export class WorkPackageRelationsCreateComponent {
@Input() readonly workPackage:WorkPackageResource;
@ViewChild('focusAfterSave') readonly focusAfterSave:ElementRef;
public showRelationsCreateForm:boolean = false;
public workPackage:WorkPackageResource;
public selectedRelationType:string = RelationResource.DEFAULT();
public selectedWpId:string;
public externalFormToggle:boolean;
public fixedRelationType:string;
public relationTypes = RelationResource.LOCALIZED_RELATION_TYPES(false);
public isDisabled = false;
constructor(protected I18n:op.I18n,
protected $scope:ng.IScope,
protected $rootScope:ng.IRootScopeService,
protected $element:ng.IAugmentedJQuery,
protected $timeout:ng.ITimeoutService,
protected wpRelations:WorkPackageRelationsService,
protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpCacheService:WorkPackageCacheService) {
}
$onInit() {
if (this.fixedRelationType) {
this.selectedRelationType = this.fixedRelationType;
}
if (this.externalFormToggle) {
this.showRelationsCreateForm = this.externalFormToggle;
}
}
public text = {
save: this.I18n.t('js.relation_buttons.save'),
abort: this.I18n.t('js.relation_buttons.abort'),
addNewRelation: this.I18n.t('js.relation_buttons.add_new_relation')
};
constructor(@Inject(I18nToken) protected I18n:op.I18n,
protected wpRelations:WorkPackageRelationsService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpCacheService:WorkPackageCacheService) {
}
public createRelation() {
if (!this.selectedRelationType || !this.selectedWpId) {
@ -57,6 +46,10 @@ export class WorkPackageRelationsCreateController {
.then(() => this.isDisabled = false);
}
public updateSelectedId(workPackageId:string) {
this.selectedWpId = workPackageId;
}
protected async createCommonRelation() {
return this.wpRelations.addCommonRelation(this.workPackage,
this.selectedRelationType,
@ -73,36 +66,13 @@ export class WorkPackageRelationsCreateController {
public toggleRelationsCreateForm() {
this.showRelationsCreateForm = !this.showRelationsCreateForm;
this.externalFormToggle = !this.externalFormToggle;
this.$timeout(() => {
setTimeout(() => {
if (!this.showRelationsCreateForm) {
// Reset value
this.selectedWpId = '';
this.$element.find('.-focus-after-save').first().focus();
this.focusAfterSave.nativeElement.focus();
}
});
}
}
function wpRelationsCreate():any {
return {
restrict: 'E',
templateUrl: (el:ng.IAugmentedJQuery, attrs:ng.IAttributes) => {
return '/components/wp-relations/wp-relations-create/' + attrs['template'] + '.template.html';
},
scope: {
workPackage: '=?',
fixedRelationType: '@?',
externalFormToggle: '=?'
},
controller: WorkPackageRelationsCreateController,
bindToController: true,
controllerAs: '$ctrl',
};
}
wpDirectivesModule.directive('wpRelationsCreate', wpRelationsCreate);

@ -26,35 +26,48 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {wpDirectivesModule} from '../../angular-modules';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
/**
* Contains methods and attributes shared
* between common relations and parent-child relations
*/
export class WorkPackageSingleRelationController {
public workPackagePath = this.PathHelper.workPackagePath.bind(this.PathHelper);
constructor(protected PathHelper:PathHelperService) {
import {Component, ElementRef, EventEmitter, Inject, Input, Output, ViewChild} from "@angular/core";
import {I18nToken} from "core-app/angular4-transition-utils";
@Component({
selector: 'wp-relations-group',
template: require('!!raw-loader!./wp-relations-group.template.html')
})
export class WorkPackageRelationsGroupComponent {
@Input() public relatedWorkPackages:WorkPackageResource[];
@Input() public workPackage:WorkPackageResource;
@Input() public header:string;
@Input() public firstGroup:boolean;
@Input() public groupByWorkPackageType:boolean;
@Output() public onToggleGroupBy = new EventEmitter<undefined>();
@ViewChild('wpRelationGroupByToggler') readonly toggleElement:ElementRef;
public text = {
groupByType: this.I18n.t('js.relation_buttons.group_by_wp_type'),
groupByRelation: this.I18n.t('js.relation_buttons.group_by_relation_type')
};
constructor(
@Inject(I18nToken) public I18n:op.I18n) {
}
public getFullIdentifier(workPackage:WorkPackageResource, hideType?:boolean) {
if (hideType) {
return workPackage.subject;
public get togglerText() {
if (this.groupByWorkPackageType) {
return this.text.groupByRelation;
} else {
return this.text.groupByType;
}
return workPackage.subjectWithType;
}
}
function wpSingleRelationDirective():any {
return {
restrict: 'A',
controller: WorkPackageSingleRelationController,
controllerAs: 'singleRelationCtrl',
bindToController: true,
};
}
public toggleButton() {
this.onToggleGroupBy.emit();
wpDirectivesModule.directive('wpSingleRelation', wpSingleRelationDirective);
setTimeout(() => {
this.toggleElement.nativeElement.focus();
}, 20);
}
}

@ -1,86 +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 {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageRelationsController} from '../wp-relations.directive';
export class WorkPackageRelationsGroupController {
public relatedWorkPackages:Array<WorkPackageResource>;
public workPackage:WorkPackageResource;
public header:string;
public firstGroup:boolean;
public groupByWorkPackageType:boolean;
public text:Object;
public relationsCtrl: WorkPackageRelationsController;
constructor(public $element:ng.IAugmentedJQuery,
public $timeout:ng.ITimeoutService,
public I18n:op.I18n) {
this.text = {
groupByType: I18n.t('js.relation_buttons.group_by_wp_type'),
groupByRelation: I18n.t('js.relation_buttons.group_by_relation_type')
};
}
public toggleButton() {
this.relationsCtrl.toggleGroupBy();
this.$timeout(() => {
this.$element.find('#wp-relation-group-by-toggle').focus();
});
}
}
function wpRelationsGroupDirective():any {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relations-group/wp-relations-group.template.html',
scope: {
header: '=',
firstGroup: '=',
workPackage: '=',
groupByWorkPackageType: '=',
relatedWorkPackages: '='
},
link: (scope:any,
element:ng.IAugmentedJQuery,
attrs:any,
controllers: [WorkPackageRelationsController]) => {
scope.$ctrl.relationsCtrl = controllers[0];
},
controller: WorkPackageRelationsGroupController,
controllerAs: '$ctrl',
require: ['^wpRelations'],
bindToController: true,
};
}
wpDirectivesModule.directive('wpRelationsGroup', wpRelationsGroupDirective);

@ -1,28 +1,30 @@
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text relation-group--header">
{{ $ctrl.header }}
</h3>
</div>
<div class="attributes-group--header-toggle" ng-if="$ctrl.firstGroup">
<div id="wp-relation-group-by-toggle"
class="panel-toggler ng-scope ng-isolate-scope hide-when-print">
<accessible-by-keyboard link-class="icon-context icon-small icon-group-by button -transparent"
execute="$ctrl.toggleButton()">
<span ng-if="!$ctrl.groupByWorkPackageType" ng-bind="::$ctrl.text.groupByType"></span>
<span ng-if="$ctrl.groupByWorkPackageType" ng-bind="::$ctrl.text.groupByRelation"></span>
</accessible-by-keyboard>
</div>
</div>
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text relation-group--header"
[textContent]="header">
</h3>
</div>
<div class="content" ng-if="$ctrl.relatedWorkPackages">
<wp-relation-row
work-package="$ctrl.workPackage"
group-by-work-package-type="$ctrl.groupByWorkPackageType"
related-work-package="relatedWorkPackage"
ng-repeat="relatedWorkPackage in $ctrl.relatedWorkPackages"></wp-relation-row>
<div class="attributes-group--header-toggle"
*ngIf="firstGroup">
<div id="wp-relation-group-by-toggle"
#wpRelationGroupByToggler
class="panel-toggler ng-scope ng-isolate-scope hide-when-print">
<accessible-by-keyboard linkClass="icon-context icon-small icon-group-by button -transparent"
(execute)="toggleButton()">
<span [textContent]="togglerText"></span>
</accessible-by-keyboard>
</div>
</div>
</div>
<div class="content"
*ngIf="relatedWorkPackages">
<wp-relation-row
*ngFor="let relatedWorkPackage of relatedWorkPackages"
[workPackage]="workPackage"
[groupByWorkPackageType]="groupByWorkPackageType"
[relatedWorkPackage]="relatedWorkPackage"></wp-relation-row>
</div>
</div>

@ -1,112 +0,0 @@
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageRelationsHierarchyService} from '../wp-relations-hierarchy/wp-relations-hierarchy.service';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
class WpRelationsHierarchyRowDirectiveController {
public workPackage:WorkPackageResource;
public relatedWorkPackage:WorkPackageResource;
public relationType:any;
public showEditForm:boolean = false;
public workPackagePath = this.PathHelper.workPackagePath.bind(this.PathHelper);
public canModifyHierarchy:boolean = false;
constructor(protected $scope:ng.IScope,
protected $timeout:ng.ITimeoutService,
protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
protected wpCacheService:WorkPackageCacheService,
protected wpNotificationsService:WorkPackageNotificationService,
protected PathHelper:PathHelperService,
protected I18n:op.I18n,
protected $q:ng.IQService) {
this.canModifyHierarchy = !!this.workPackage.changeParent;
if (this.relatedWorkPackage) {
scopedObservable($scope, this.wpCacheService.state(this.relatedWorkPackage.id).values$())
.subscribe((wp) => this.relatedWorkPackage = wp);
}
}
public text = {
change_parent:this.I18n.t('js.relation_buttons.change_parent'),
remove_parent:this.I18n.t('js.relation_buttons.remove_parent'),
remove_child:this.I18n.t('js.relation_buttons.remove_child'),
remove:this.I18n.t('js.relation_buttons.remove'),
parent:this.I18n.t('js.relation_labels.parent'),
children:this.I18n.t('js.relation_labels.children')
};
public get relationReady() {
return this.relatedWorkPackage && this.relatedWorkPackage.$loaded;
}
public get relationClassName() {
if (this.isCurrentElement()) {
return 'self';
}
return this.relationType;
}
public removeRelation() {
if (this.relationType === 'child') {
this.removeChild();
} else if (this.relationType === 'parent') {
this.removeParent();
}
}
public isCurrentElement():boolean {
return (this.relationType !== 'child' && this.relationType !== 'parent');
}
public isParent() {
return this.relationType === 'parent';
}
protected removeChild() {
this.wpRelationsHierarchyService
.removeChild(this.relatedWorkPackage)
.then(() => {
this.wpCacheService.loadWorkPackage(this.workPackage.id, true);
this.wpNotificationsService.showSave(this.workPackage);
this.$timeout(() => {
angular.element('#hierarchy--add-exisiting-child').focus();
});
});
}
protected removeParent() {
this.wpRelationsHierarchyService
.removeParent(this.workPackage)
.then(() => {
this.wpNotificationsService.showSave(this.workPackage);
this.$timeout(() => {
angular.element('#hierarchy--add-parent').focus();
});
});
}
}
function WpRelationsHierarchyRowDirective():any {
return {
restrict:'E',
templateUrl:'/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.template.html',
scope:{
indentBy:'@?',
workPackage:'=',
relatedWorkPackage:'=?',
relationType:'@'
},
controller:WpRelationsHierarchyRowDirectiveController,
controllerAs:'$ctrl',
bindToController:true
};
}
wpDirectivesModule.directive('wpRelationsHierarchyRow', WpRelationsHierarchyRowDirective);

@ -102,8 +102,3 @@ export class WorkPackageRelationsHierarchyComponent implements OnInit, OnDestroy
// nothing to do
}
}
opWorkPackagesModule.directive(
'wpRelationsHierarchy',
downgradeComponent({ component: WorkPackageRelationsHierarchyComponent })
);

@ -1,24 +1,24 @@
<div class="wp-relations-hierarchy-section">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.parentHeadline">
</h3>
</div>
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.parentHeadline">
</h3>
</div>
</div>
<wp-relation-parent [workPackage]="workPackage"></wp-relation-parent>
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.childrenHeadline">
</h3>
</div>
<wp-relation-parent [workPackage]="workPackage"></wp-relation-parent>
</div>
<div class="wp-relations-hierarchy-section wp-relations--children">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.childrenHeadline">
</h3>
</div>
<wp-children-query
[workPackage]="workPackage"
[query]="childrenQueryProps">
</wp-children-query>
</div>
<wp-children-query
[workPackage]="workPackage"
[query]="childrenQueryProps">
</wp-children-query>
</div>

@ -9,7 +9,7 @@
class="wp-relations--subject-field"
[attr.aria-label]="text.parent"
[attr.id]="wp-relations-parent-element"
[textContent]="workPackage.parent.subjectWithType">
[textContent]="workPackage.parent.subjectWithType()">
</a>
</span>
</div>

@ -26,23 +26,28 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {StateService} from '@uirouter/core';
import {Observable} from 'rxjs/Observable';
import {zip} from 'rxjs/observable/zip';
import {take} from 'rxjs/operators';
import {wpDirectivesModule} from '../../angular-modules';
import {scopedObservable} from '../../helpers/angular-rx-utils';
import {take, takeUntil} from 'rxjs/operators';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {RelatedWorkPackagesGroup} from './wp-relations.interfaces';
import {RelationsStateValue, WorkPackageRelationsService} from './wp-relations.service';
export class WorkPackageRelationsController {
public relationGroups:RelatedWorkPackagesGroup;
import {Component, Inject, Input, OnDestroy, OnInit} from "@angular/core";
import {I18nToken} from "core-app/angular4-transition-utils";
import {componentDestroyed} from "ng2-rx-componentdestroyed";
@Component({
selector: 'wp-relations',
template: require('!!raw-loader!./wp-relations.template.html')
})
export class WorkPackageRelationsComponent implements OnInit, OnDestroy {
@Input() public workPackage:WorkPackageResource;
public relationGroups:RelatedWorkPackagesGroup = {};
public relationGroupKeys:string[] = [];
public relationsPresent:boolean = false;
public workPackage:WorkPackageResource;
public canAddRelation:boolean;
// By default, group by relation type
@ -53,19 +58,19 @@ export class WorkPackageRelationsController {
};
public currentRelations:WorkPackageResource[] = [];
constructor(protected $scope:ng.IScope,
protected $q:ng.IQService,
protected $state:StateService,
protected I18n:op.I18n,
protected wpRelations:WorkPackageRelationsService,
protected wpCacheService:WorkPackageCacheService) {
constructor(
@Inject(I18nToken) readonly I18n:op.I18n,
readonly wpRelations:WorkPackageRelationsService,
readonly wpCacheService:WorkPackageCacheService) {
}
$onInit() {
ngOnInit() {
this.canAddRelation = !!this.workPackage.addRelation;
scopedObservable(this.$scope,
this.wpRelations.state(this.workPackage.id).values$())
this.wpRelations.state(this.workPackage.id).values$()
.pipe(
takeUntil(componentDestroyed(this))
)
.subscribe((relations:RelationsStateValue) => {
this.loadedRelations(relations);
});
@ -73,16 +78,22 @@ export class WorkPackageRelationsController {
this.wpRelations.require(this.workPackage.id);
// Listen for changes to this WP.
scopedObservable(this.$scope,
this.wpCacheService.loadWorkPackage(this.workPackage.id).values$())
this.wpCacheService.loadWorkPackage(this.workPackage.id).values$()
.pipe(
takeUntil(componentDestroyed(this))
)
.subscribe((wp:WorkPackageResource) => {
this.workPackage = wp;
});
}
ngOnDestroy() {
// Nothing to do, interface compliance.
}
private getRelatedWorkPackages(workPackageIds:string[]):Observable<WorkPackageResource[]> {
let observablesToGetZipped:Observable<WorkPackageResource>[] = workPackageIds.map(wpId => {
return scopedObservable(this.$scope, this.wpCacheService.loadWorkPackage(wpId).values$());
return this.wpCacheService.loadWorkPackage(wpId).values$();
});
return zip(...observablesToGetZipped);
@ -112,6 +123,7 @@ export class WorkPackageRelationsController {
return this.I18n.t('js.relation_labels.' + normalizedType);
}
});
this.relationGroupKeys = _.keys(this.relationGroups);
this.relationsPresent = _.size(this.relationGroups) > 0;
}
@ -144,20 +156,3 @@ export class WorkPackageRelationsController {
});
}
}
function wpRelationsDirective():any {
return {
restrict: 'E',
templateUrl: '/components/wp-relations/wp-relations.template.html',
scope: {
workPackage: '='
},
controller: WorkPackageRelationsController,
controllerAs: '$ctrl',
bindToController: true
};
}
wpDirectivesModule.directive('wpRelations', wpRelationsDirective);

@ -1,11 +1,4 @@
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
export interface RelatedWorkPackagesGroup {
[key: string] : any;
}
export interface RelationTitle {
[key: string]: string;
[key:string]:any;
}

@ -6,6 +6,7 @@ import {StateCacheService} from '../states/state-cache.service';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
import {RelationsDmService} from 'core-app/modules/hal/dm-services/relations-dm.service';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {Injectable} from "@angular/core";
export type RelationsStateValue = { [relationId:number]:RelationResource };
@ -20,6 +21,7 @@ class RelationStateGroup extends StatesGroup {
}
}
@Injectable()
export class WorkPackageRelationsService extends StateCacheService<RelationsStateValue> {
private relationStates:RelationStateGroup;
@ -27,7 +29,6 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
/*@ngInject*/
constructor(private relationsDm:RelationsDmService,
private wpTableRefresh:WorkPackageTableRefreshService,
private $q:ng.IQService,
private PathHelper:PathHelperService) {
super();
this.relationStates = new RelationStateGroup();
@ -49,22 +50,21 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
this.updateRelationsStateTo(id, elements);
resolve(this.state(id).value!);
})
.catch((error) => reject(error));
.catch(reject);
});
}
protected loadAll(ids:string[]) {
const deferred = this.$q.defer<undefined>();
this.relationsDm
.loadInvolved(ids)
.then((elements:RelationResource[]) => {
this.clearSome(...ids);
this.accumulateRelationsFromInvolved(ids, elements);
deferred.resolve();
});
return deferred.promise;
protected async loadAll(ids:string[]) {
return new Promise<undefined>((resolve, reject) => {
this.relationsDm
.loadInvolved(ids)
.then((elements:RelationResource[]) => {
this.clearSome(...ids);
this.accumulateRelationsFromInvolved(ids, elements);
resolve();
})
.catch(reject);
});
}
/**
@ -95,7 +95,7 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
return this.updateRelation(relation, params);
}
public async updateRelation(relation:RelationResource, params:{[key:string]: any}) {
public async updateRelation(relation:RelationResource, params:{[key:string]:any}) {
return relation.updateImmediately(params)
.then((savedRelation:RelationResource) => {
this.insertIntoStates(savedRelation);
@ -186,7 +186,4 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
});
}
}
opServicesModule.service('wpRelations', WorkPackageRelationsService);

@ -1,29 +1,29 @@
<div class="loading-indicator--location"
data-indicator-name="relation-groups">
<div class="wp-relations-hierarchy-section"
ng-if="!$ctrl.relationsPresent">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="$ctrl.text.relations_header">
</h3>
</div>
</div>
<div class="wp-relations-hierarchy-section"
*ngIf="!relationsPresent">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.relations_header">
</h3>
</div>
</div>
</div>
<div ng-repeat="(type, relatedWorkPackages) in $ctrl.relationGroups">
<wp-relations-group header="type"
group-by-work-package-type="$ctrl.groupByWorkPackageType"
first-group="$first"
related-work-packages="relatedWorkPackages"
work-package="$ctrl.workPackage"></wp-relations-group>
</div>
<wp-relations-create related-work-packages="$ctrl.currentRelations"
ng-if="$ctrl.canAddRelation"
work-package="$ctrl.workPackage"
template="dynamic-relation-types"></wp-relations-create>
<div *ngFor="let type of relationGroupKeys; let first = first">
<wp-relations-group [header]="type"
[groupByWorkPackageType]="groupByWorkPackageType"
(onToggleGroupBy)="toggleGroupBy()"
[firstGroup]="first"
[relatedWorkPackages]="relationGroups[type]"
[workPackage]="workPackage"></wp-relations-group>
</div>
<wp-relations-create *ngIf="canAddRelation"
[workPackage]="workPackage">
</wp-relations-create>
<wp-relations-hierarchy [work-package]="$ctrl.workPackage"></wp-relations-hierarchy>
<wp-relations-hierarchy [workPackage]="workPackage"></wp-relations-hierarchy>
</div>

@ -1,6 +1,6 @@
<div class="detail-panel-description detail-panel--relations detail-panel--autocomplete-target"
*ngIf="workPackage">
<div class="detail-panel-description-content">
<ng1-wp-relations-wrapper [workPackage]="workPackage"></ng1-wp-relations-wrapper>
<wp-relations [workPackage]="workPackage"></wp-relations>
</div>
</div>

@ -95,6 +95,12 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit,
// Load initial query
this.loadQuery();
// Reload results on refresh requests
this.tableState.refreshRequired
.values$()
.pipe(untilComponentDestroyed(this))
.subscribe(async () => this.refresh());
// Reload results on changes to pagination
this.tableState.ready.fireOnStateChange(this.wpTablePagination.state,
'Query loaded').values$().pipe(
@ -126,6 +132,8 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit,
if (this.configuration.projectContext) {
identifier = this.currentProject.identifier;
} else {
identifier = this.configuration.projectIdentifier;
}
return identifier || undefined;
@ -188,5 +196,5 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit,
// TODO: remove as this should also work by angular2 only
opUiComponentsModule.directive(
'wpEmbeddedTable',
downgradeComponent({ component: WorkPackageEmbeddedTableComponent })
downgradeComponent({component: WorkPackageEmbeddedTableComponent})
);

@ -27,7 +27,7 @@
// ++
export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:boolean }>;
export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:string|boolean }>;
export class WorkPackageTableConfiguration {
/** Render the table results, set to false when only wanting the table initialization */
@ -45,6 +45,9 @@ export class WorkPackageTableConfiguration {
/** Whether the query should be resolved using the current project identifier */
public projectContext:boolean = true;
/** Whether the embedded table should live within a specific project context (e.g., given by its parent) */
public projectIdentifier:string|null = null;
/** Whether inline create is enabled*/
public inlineCreateEnabled:boolean = true;
@ -57,7 +60,7 @@ export class WorkPackageTableConfiguration {
constructor(private providedConfig:WorkPackageTableConfigurationObject) {
_.each(providedConfig, (value, k) => {
let key = (k as keyof WorkPackageTableConfiguration);
this[key] = !!value;
this[key] = value as any;
});
}
}

@ -33,13 +33,13 @@ declare const I18n:op.I18n;
$(function () {
// Specifies minimum versions to be supported
// As we don't support ANY version of msie, so treat 11 (last ie before edge) as unsupported
const unsupported = {
const minimumSupported = {
msie: '12',
firefox: '52'
firefox: '60'
};
let additionalMessage = I18n.t("js.unsupported_browser.update_message");
if (bowser.isUnsupportedBrowser(unsupported, window.navigator.userAgent)) {
if (bowser.isUnsupportedBrowser(minimumSupported, window.navigator.userAgent)) {
if (bowser.msie) {
additionalMessage = I18n.t("js.unsupported_browser.update_ie_user");
}

@ -66,5 +66,3 @@ export class RelationsDmService {
.then((collection:CollectionResource<RelationResource>) => collection.elements);
}
}
opServicesModule.service('relationsDm', downgradeInjectable(RelationsDmService));

@ -146,12 +146,12 @@ export class WorkPackageResource extends HalResource {
/**
* Return "<type name>: <subject>" if the type is known.
*/
public get subjectWithType():string {
if (this.type) {
return `${this.type.name}: ${this.subject}`;
} else {
return this.subject;
}
public subjectWithType(truncateSubject:number = 40):string {
const type = this.type ? `${this.type.name}: ` : '';
const id = this.isNew ? '' : ` (#${this.id})`;
const subject = _.truncate(this.subject, { length: truncateSubject });
return `${type}${subject}${id}`;
}
public get isNew():boolean {

@ -5,7 +5,8 @@ describe 'Work package relations tab', js: true, selenium: true do
let(:user) { FactoryBot.create :admin }
let(:project) { FactoryBot.create :project }
let(:project) { FactoryBot.create(:project) }
let(:work_package) { FactoryBot.create(:work_package, project: project) }
let(:work_packages_page) { ::Pages::SplitWorkPackage.new(work_package) }
let(:full_wp) { ::Pages::FullWorkPackage.new(work_package) }
@ -56,6 +57,24 @@ describe 'Work package relations tab', js: true, selenium: true do
relations.add_existing_child(child2)
end
describe 'inline create' do
let!(:status) { FactoryBot.create(:status, is_default: true) }
let!(:priority) { FactoryBot.create(:priority, is_default: true) }
let(:type_bug) { FactoryBot.create(:type_bug) }
let!(:project) do
FactoryBot.create(:project, types: [type_bug])
end
it 'can inline-create children' do
relations.inline_create_child 'my new child'
table = relations.children_table
table.expect_work_package_subject 'my new child'
work_package.reload
expect(work_package.children.count).to eq(1)
end
end
end
describe 'relation group-by toggler' do

@ -144,6 +144,15 @@ module Components
expect(page).to have_no_selector('.relation-row--parent', text: removed_text, wait: 10)
end
def inline_create_child(subject_text)
container = find('.wp-relations--children')
scroll_to_and_click(container.find('.wp-inline-create-button-row .wp-inline-create--add-link'))
subject = ::WorkPackageField.new(container, 'subject')
subject.expect_active!
subject.update subject_text
end
def add_existing_child(work_package)
# Locate the create row container
container = find('.wp-relations--child-form')
@ -155,6 +164,10 @@ module Components
container.find('.wp-create-relation--save').click
end
def children_table
::Pages::EmbeddedWorkPackagesTable.new find('.work-packages-embedded-view--container')
end
def remove_child(work_package)
page.within('.work-packages-embedded-view--container') do
row = ".wp-row-#{work_package.id}-table"

@ -54,6 +54,14 @@ module Pages
end
end
def expect_work_package_subject(subject)
within(table_container) do
expect(page).to have_selector("td.subject",
text: subject,
wait: 20)
end
end
def expect_work_package_count(n)
within(table_container) do
expect(page).to have_selector(".wp--row", count: n, wait: 20)

Loading…
Cancel
Save