From 039cb26b6e89eea9c0d4ee2ab59b6044f6bf3498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Nov 2018 13:35:38 +0100 Subject: [PATCH] [27607] Implement parent selection in wp breadcrumb, drop subject https://community.openproject.com/wp/27607 --- .../stylesheets/layout/_breadcrumb.sass | 4 - config/locales/js-en.yml | 3 + frontend/src/app/angular4-modules.ts | 4 +- .../wp-breadcrumb-parent.component.ts | 95 +++++++++++++++++++ .../wp-breadcrumb/wp-breadcrumb-parent.html | 25 +++++ .../wp-breadcrumb/wp-breadcrumb.component.ts | 20 ++++ .../wp-breadcrumb/wp-breadcrumb.html | 27 ++++-- .../wp-breadcrumb/wp-breadcrumb.sass | 2 + .../wp-relations-count.component.ts | 5 +- ...p-inline-add-existing-child.component.html | 1 + ...lations-autocomplete.upgraded.component.ts | 35 +++++-- .../wp-relations-autocomplete.upgraded.html | 6 +- .../wp-relations-hierarchy.template.html | 11 --- .../wp-relations-parent.component.ts | 93 ------------------ .../wp-relations-parent.html | 87 ----------------- .../details/relations/hierarchy_spec.rb | 36 +++---- .../components/work_packages/relations.rb | 31 +++--- 17 files changed, 228 insertions(+), 257 deletions(-) create mode 100644 frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts create mode 100644 frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.html create mode 100644 frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.sass delete mode 100644 frontend/src/app/components/wp-relations/wp-relations-parent/wp-relations-parent.component.ts delete mode 100644 frontend/src/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html diff --git a/app/assets/stylesheets/layout/_breadcrumb.sass b/app/assets/stylesheets/layout/_breadcrumb.sass index c3a9af6874..ef947addd1 100644 --- a/app/assets/stylesheets/layout/_breadcrumb.sass +++ b/app/assets/stylesheets/layout/_breadcrumb.sass @@ -101,10 +101,6 @@ ul.breadcrumb max-width: 420px @include text-shortener - &:first-child - &:before - display: none - // This is ugly. However, this way we do not need to touch complicated // toolbar-container positioning. body.action-show .wp-breadcrumb diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index e5909ce8c3..7b045ecdb9 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -396,9 +396,11 @@ en: relations_hierarchy: parent_headline: "Parent" + hierarchy_headline: "Hierarchy" children_headline: "Children" relation_buttons: + set_parent: "Set parent work package" change_parent: "Change parent" remove_parent: "Remove parent" group_by_wp_type: "Group by work package type" @@ -419,6 +421,7 @@ en: relations_autocomplete: placeholder: "Enter the related work package id" + parent_placeholder: "Choose new parent, press enter to unset, escape to cancel." repositories: select_tag: 'Select tag' diff --git a/frontend/src/app/angular4-modules.ts b/frontend/src/app/angular4-modules.ts index 903798d477..cf346b1d75 100644 --- a/frontend/src/app/angular4-modules.ts +++ b/frontend/src/app/angular4-modules.ts @@ -124,7 +124,6 @@ import {WorkPackageQuerySelectableTitleComponent} from 'core-components/wp-query import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper'; import {WorkPackageRelationsHierarchyComponent} from 'core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive'; import {WorkPackageRelationsHierarchyService} from 'core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service'; -import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relations-parent/wp-relations-parent.component'; import {WorkPackageRelationsService} from 'core-components/wp-relations/wp-relations.service'; import {NewestActivityOnOverviewComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-on-overview.component'; import {WorkPackageActivityTabComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-tab.component'; @@ -226,6 +225,7 @@ import {WorkPackagesCalendarComponent} from './components/routing/wp-calendar/wp import {FullCalendarModule} from 'ng-fullcalendar'; import {WorkPackagesCalendarController} from "core-components/wp-calendar/wp-calendar.component"; import {WorkPackagesEmbeddedCalendarEntryComponent} from "core-components/wp-table/embedded/wp-embedded-calendar-entry.component"; +import {WorkPackageBreadcrumbParentComponent} from './components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component'; @NgModule({ imports: [ @@ -386,6 +386,7 @@ import {WorkPackagesEmbeddedCalendarEntryComponent} from "core-components/wp-tab WorkPackageRelationsCountComponent, WorkPackageWatchersCountComponent, WorkPackageBreadcrumbComponent, + WorkPackageBreadcrumbParentComponent, WorkPackageEditFieldGroupComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, @@ -421,7 +422,6 @@ import {WorkPackagesEmbeddedCalendarEntryComponent} from "core-components/wp-tab WorkPackageRelationsCreateComponent, WorkPackageRelationsHierarchyComponent, WpRelationsAutocompleteComponent, - WpRelationParentComponent, // Watchers tab WorkPackageWatchersTabComponent, diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts new file mode 100644 index 0000000000..2023e14fbe --- /dev/null +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts @@ -0,0 +1,95 @@ +// -- 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 {Component, Input, EventEmitter, Output} from '@angular/core'; +import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; +import {WorkPackageRelationsHierarchyService} from 'core-app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service'; +import {WorkPackageNotificationService} from 'core-app/components/wp-edit/wp-notification.service'; +import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; + +@Component({ + templateUrl: './wp-breadcrumb-parent.html', + selector: 'wp-breadcrumb-parent', +}) +export class WorkPackageBreadcrumbParentComponent { + @Input('workPackage') workPackage:WorkPackageResource; + @Output('onSwitch') onSwitch = new EventEmitter(); + + public isSaving = false; + public text = { + edit_parent: this.I18n.t('js.relation_buttons.change_parent'), + set_or_remove_parent: this.I18n.t('js.relations_autocomplete.parent_placeholder'), + set_parent: this.I18n.t('js.relation_buttons.set_parent'), + }; + + private editing:boolean; + + public constructor( + protected readonly I18n:I18nService, + protected readonly wpRelationsHierarchy:WorkPackageRelationsHierarchyService, + protected readonly wpNotifications:WorkPackageNotificationService + ) { + } + + public canModifyParent():boolean { + return !!this.workPackage.changeParent; + } + + public get parent() { + return this.workPackage && this.workPackage.parent; + } + + public get active():boolean { + return this.editing; + } + + public toggle():void { + this.editing = !this.editing; + this.onSwitch.emit(this.editing); + } + + public updateParent(newParentId:string|null) { + this.toggle(); + if (_.isNil(newParentId)) { + newParentId = null; + } + + if (_.get(this.parent, 'id', null) === newParentId) { + return; + } + + this.isSaving = true; + this.wpRelationsHierarchy.changeParent(this.workPackage, newParentId) + .catch((error:any) => { + this.wpNotifications.handleRawError(error, this.workPackage); + }) + .then(() => this.isSaving = false); // Behaves as .finally() + } +} + + diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.html b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.html new file mode 100644 index 0000000000..3821e412fd --- /dev/null +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.html @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.component.ts b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.component.ts index c7e697fa26..2dfaa2d982 100644 --- a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.component.ts +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.component.ts @@ -28,13 +28,33 @@ import {Component, Input} from '@angular/core'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; +import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; @Component({ templateUrl: './wp-breadcrumb.html', + styleUrls: ['./wp-breadcrumb.sass'], selector: 'wp-breadcrumb', }) export class WorkPackageBreadcrumbComponent { @Input('workPackage') workPackage:WorkPackageResource; + + public text = { + parent: this.I18n.t('js.relations_hierarchy.parent_headline'), + hierarchy: this.I18n.t('js.relations_hierarchy.hierarchy_headline'), + }; + + constructor(private I18n:I18nService) { + } + + public inputActive:boolean = false; + + public get hierarchyCount() { + return this.workPackage.ancestors.length; + } + + public get hierarchyLabel() { + return (this.hierarchyCount === 1) ? this.text.parent : this.text.hierarchy; + } } diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.html b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.html index c630eb1d75..4bfb68a0b7 100644 --- a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.html +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.html @@ -1,15 +1,22 @@ diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.sass b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.sass new file mode 100644 index 0000000000..ee180f144f --- /dev/null +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.sass @@ -0,0 +1,2 @@ +.active-parent-select + width: 100% diff --git a/frontend/src/app/components/work-packages/wp-relations-count/wp-relations-count.component.ts b/frontend/src/app/components/work-packages/wp-relations-count/wp-relations-count.component.ts index 1c2c2ea417..cd163822b0 100644 --- a/frontend/src/app/components/work-packages/wp-relations-count/wp-relations-count.component.ts +++ b/frontend/src/app/components/work-packages/wp-relations-count/wp-relations-count.component.ts @@ -1,7 +1,7 @@ import {Component, Input, OnDestroy, OnInit} from '@angular/core'; import {componentDestroyed} from 'ng2-rx-componentdestroyed'; import {takeUntil} from 'rxjs/operators'; -import {RelationsStateValue, WorkPackageRelationsService} from '../../wp-relations/wp-relations.service'; +import {WorkPackageRelationsService} from '../../wp-relations/wp-relations.service'; import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; import {combineLatest} from 'rxjs'; @@ -27,10 +27,9 @@ export class WorkPackageRelationsCountComponent implements OnInit, OnDestroy { takeUntil(componentDestroyed(this)) ).subscribe(([relations, workPackage]) => { let relationCount = _.size(relations); - let parentCount = workPackage.parent ? 1 : 0; let childrenCount = _.size(workPackage.children); - this.count = relationCount + parentCount + childrenCount; + this.count = relationCount + childrenCount; }); } diff --git a/frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.html b/frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.html index f33a7d2b2a..708a6fd025 100644 --- a/frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.html +++ b/frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.html @@ -4,6 +4,7 @@
diff --git a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component.ts b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component.ts index 119c27c791..a595c2f22c 100644 --- a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component.ts @@ -38,16 +38,20 @@ import {CollectionResource} from 'core-app/modules/hal/resources/collection-reso templateUrl: './wp-relations-autocomplete.upgraded.html' }) export class WpRelationsAutocompleteComponent implements OnInit { + readonly text = { + placeholder: this.I18n.t('js.relations_autocomplete.placeholder') + }; + @Input() workPackage:WorkPackageResource; @Input() loadingPromiseName:string; @Input() selectedRelationType:string; @Input() filterCandidatesFor:string; + @Input() initialSelection?:WorkPackageResource; + @Input() inputPlaceholder:string = this.text.placeholder; + @Input() appendToContainer:string = '#content'; - @Output('onWorkPackageIdSelected') public onSelect = new EventEmitter(); - - readonly text = { - placeholder: this.I18n.t('js.relations_autocomplete.placeholder') - }; + @Output('onWorkPackageIdSelected') public onSelect = new EventEmitter(); + @Output('onEscape') public onEscapePressed = new EventEmitter(); public options:any = []; public relatedWps:any = []; @@ -67,16 +71,25 @@ export class WpRelationsAutocompleteComponent implements OnInit { let input = this.$element.find('.wp-relations--autocomplete'); let selected = false; + if (this.initialSelection) { + input.val(this.getIdentifier(this.initialSelection)); + } + input.autocomplete({ delay: 250, autoFocus: false, // Accessibility! - appendTo: '#content', + appendTo: this.appendToContainer, classes: { 'ui-autocomplete': 'wp-relations-autocomplete--results' }, source: (request:{ term:string }, response:Function) => { this.autocompleteWorkPackages(request.term).then((values) => { selected = false; + + if (this.initialSelection) { + values.unshift(this.initialSelection); + } + response(values.map(wp => { return {workPackage: wp, value: this.getIdentifier(wp)}; })); @@ -87,11 +100,19 @@ export class WpRelationsAutocompleteComponent implements OnInit { this.onSelect.emit(ui.item.workPackage.id); }, minLength: 0 - }).focus(() => !selected && input.autocomplete('search', input.val())); + }) + .focus(() => !selected && input.autocomplete('search', input.val())); setTimeout(() => input.focus(), 20); } + public handleEnterPressed($event:KeyboardEvent) { + const val = ($event.target as HTMLInputElement).value; + if (!val) { + this.onSelect.emit(null); + } + } + private getIdentifier(workPackage:WorkPackageResource):string { if (workPackage) { return `#${workPackage.id} - ${workPackage.subject}`; diff --git a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.html b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.html index fcf93aef39..1c6bb707f5 100644 --- a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.html +++ b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.html @@ -1,8 +1,10 @@
+ [ngClass]="{ '-error': noResults && !initialSelection }" + (keydown.enter)="handleEnterPressed($event)" + (keydown.escape)="onEscapePressed.emit($event)" + [attr.placeholder]="inputPlaceholder">