Merge pull request #6788 from opf/feature/inline-create-referencable

Allow inline-create component to receive dynamic component to reference existing work packages

[ci skip]
pull/6795/head
Oliver Günther 6 years ago committed by GitHub
commit 9f51ec29f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/assets/stylesheets/content/work_packages/_table_inline_create.sass
  2. 3
      app/assets/stylesheets/layout/work_packages/_table_embedded.sass
  3. 9
      frontend/src/app/angular4-modules.ts
  4. 60
      frontend/src/app/components/wp-inline-create/wp-inline-create.component.html
  5. 53
      frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts
  6. 90
      frontend/src/app/components/wp-inline-create/wp-inline-create.service.ts
  7. 29
      frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.html
  8. 96
      frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component.ts
  9. 78
      frontend/src/app/components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.service.ts
  10. 60
      frontend/src/app/components/wp-relations/wp-relation-add-child/wp-relation-add-child.html
  11. 88
      frontend/src/app/components/wp-relations/wp-relation-add-child/wp-relation-add-child.ts
  12. 25
      frontend/src/app/components/wp-relations/wp-relation-children/wp-children-query.component.ts
  13. 5
      frontend/src/app/components/wp-relations/wp-relation-children/wp-children-query.html
  14. 2
      frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component.ts
  15. 2
      frontend/src/app/modules/common/i18n/i18n.service.ts
  16. 2
      lib/api/v3/work_packages/work_package_representer.rb
  17. 6
      spec/features/work_packages/details/relations/hierarchy_spec.rb
  18. 176
      spec/features/work_packages/new/create_child_spec.rb
  19. 2
      spec/support/components/work_packages/relations.rb

@ -11,14 +11,19 @@
a
width: 100%
padding: 0.5rem 0
display: block
display: inline-block
line-height: 1.6
a.wp-inline-create--split-link
width: 50%
&:hover
background: #e4f7fb
.wp-inline-create--add-link
.wp-inline-create--add-link,
.wp-inline-create--reference-link
font-weight: bold
.icon::before

@ -72,7 +72,8 @@ $table-timeline--compact-row-height: 28px
padding-bottom: 2px
// Fix inline create heights
.wp-inline-create--add-link
.wp-inline-create--add-link,
.wp-inline-create--reference-link
height: $table-timeline--compact-row-height
padding: 0
line-height: 2

@ -122,7 +122,6 @@ import {WorkPackageNewSplitViewComponent} from 'core-components/wp-new/wp-new-sp
import {WorkPackageQuerySelectDropdownComponent} from 'core-components/wp-query-select/wp-query-select-dropdown.component';
import {WorkPackageQuerySelectableTitleComponent} from 'core-components/wp-query-select/wp-query-selectable-title.component';
import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper';
import {WpRelationAddChildComponent} from 'core-components/wp-relations/wp-relation-add-child/wp-relation-add-child';
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';
@ -221,6 +220,8 @@ import {WorkPackageEmbeddedGraphComponent} from "core-components/wp-table/embedd
import {WorkPackageByVersionGraphComponent} from "core-components/wp-by-version-graph/wp-by-version-graph.component";
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {WorkPackageCommentFieldComponent} from "core-components/work-packages/work-package-comment/wp-comment-field.component";
import {WorkPackageInlineAddExistingChildService} from "core-components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.service";
import {WorkPackageInlineAddExistingChildComponent} from "core-components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component";
@NgModule({
imports: [
@ -303,6 +304,7 @@ import {WorkPackageCommentFieldComponent} from "core-components/work-packages/wo
// Provide a separate service for creation events of WP Inline create
// This can be hierarchically injected to provide isolated events on an embedded table
WorkPackageInlineCreateService,
WorkPackageInlineAddExistingChildService,
OpTableActionsService,
CurrentProjectService,
@ -412,7 +414,6 @@ import {WorkPackageCommentFieldComponent} from "core-components/work-packages/wo
WorkPackageRelationsCreateComponent,
WorkPackageRelationsHierarchyComponent,
WpRelationsAutocompleteComponent,
WpRelationAddChildComponent,
WpRelationParentComponent,
// Watchers tab
@ -447,6 +448,7 @@ import {WorkPackageCommentFieldComponent} from "core-components/work-packages/wo
// Inline create
WorkPackageInlineCreateComponent,
WorkPackageInlineAddExistingChildComponent,
// Embedded table
WorkPackageEmbeddedTableComponent,
@ -528,6 +530,9 @@ import {WorkPackageCommentFieldComponent} from "core-components/work-packages/wo
WorkPackageEditFieldGroupComponent,
WorkPackageCommentFieldComponent,
// Inline create
WorkPackageInlineAddExistingChildComponent,
// Searchbar
ExpandableSearchComponent,

@ -1,18 +1,42 @@
<tr *ngIf="!isHidden && isAllowed"
class="wp-inline-create-button-row hide-when-print">
<td [attr.colspan]="colspan"
class="wp-inline-create-button-td -no-highlighting">
<div class="wp-inline-create-button">
<a class="wp-inline-create--add-link"
href
role="link"
[focus]="focus"
(accessibleClick)="handleAddRowClick()"
[attr.disabled]="!isAllowed || undefined"
[attr.aria-label]="text.create"
aria-haspopup="true">
<op-icon icon-classes="icon icon-add"></op-icon>
</a>
</div>
</td>
</tr>
<ng-container *ngIf="canAdd || canReference">
<tr *ngIf="mode === 'inactive'"
class="wp-inline-create-button-row hide-when-print">
<td [attr.colspan]="colspan"
class="wp-inline-create-button-td -no-highlighting">
<div class="wp-inline-create-button">
<a class="wp-inline-create--add-link"
href
role="link"
[focus]="focus"
*ngIf="canAdd"
[ngClass]="{'wp-inline-create--split-link': hasReferenceClass }"
(accessibleClick)="handleAddRowClick()"
[attr.disabled]="!isAllowed || undefined"
[attr.aria-label]="text.create"
aria-haspopup="true">
<op-icon icon-classes="icon icon-add"></op-icon>
<span [textContent]="text.create"></span>
</a>
<ng-container *ngIf="canReference">
<a class="wp-inline-create--reference-link wp-inline-create--split-link"
href
role="link"
(accessibleClick)="handleReferenceClick()"
[attr.disabled]="!isAllowed || undefined"
[attr.aria-label]="text.create"
aria-haspopup="true">
<op-icon icon-classes="icon icon-add"></op-icon>
<span [textContent]="text.reference"></span>
</a>
</ng-container>
</div>
</td>
</tr>
<tr *ngIf="hasReferenceClass && mode === 'reference'"
class="wp-inline-create-button-row hide-when-print">
<td [attr.colspan]="colspan"
class="wp-inline-reference-row-td -no-highlighting">
<ng-container *ngComponentOutlet="referenceClass; injector: injector"></ng-container>
</td>
</tr>
</ng-container>

@ -28,7 +28,8 @@
import {
Component,
ElementRef, HostListener,
ElementRef,
HostListener,
Inject,
Injector,
Input,
@ -38,7 +39,7 @@ import {
} from '@angular/core';
import {AuthorisationService} from 'core-app/modules/common/model-auth/model-auth.service';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {filter, takeUntil} from 'rxjs/operators';
import {filter} from 'rxjs/operators';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {TableRowEditContext} from '../wp-edit-form/table-row-edit-context';
@ -49,7 +50,6 @@ import {WorkPackageFilterValues} from '../wp-edit-form/work-package-filter-value
import {TimelineRowBuilder} from '../wp-fast-table/builders/timeline/timeline-row-builder';
import {onClickOrEnter} from '../wp-fast-table/handlers/click-or-enter-handler';
import {WorkPackageTableColumnsService} from '../wp-fast-table/state/wp-table-columns.service';
import {WorkPackageTableFiltersService} from '../wp-fast-table/state/wp-table-filters.service';
import {WorkPackageTable} from '../wp-fast-table/wp-fast-table';
import {WorkPackageCreateService} from '../wp-new/wp-create.service';
import {
@ -58,7 +58,7 @@ import {
inlineCreateRowClassName
} from './inline-create-row-builder';
import {TableState} from 'core-components/wp-table/table-state/table-state';
import {componentDestroyed, untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {IWorkPackageEditingServiceToken} from "../wp-edit-form/work-package-editing.service.interface";
@ -77,13 +77,12 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
// inner state
public isHidden:boolean = false;
// Inline create / reference row is active
public mode:'inactive'|'create'|'reference' = 'inactive';
public focus:boolean = false;
public text = {
create: this.I18n.t('js.label_create_work_package')
};
public text = this.wpInlineCreate.buttonTexts;
private currentWorkPackage:WorkPackageResource | null;
@ -95,8 +94,8 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
private $element:JQuery;
constructor(protected readonly elementRef:ElementRef,
protected readonly injector:Injector,
constructor(public readonly injector:Injector,
protected readonly elementRef:ElementRef,
protected readonly FocusHelper:FocusHelperService,
protected readonly I18n:I18nService,
protected readonly tableState:TableState,
@ -118,6 +117,10 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
this.$element = jQuery(this.elementRef.nativeElement);
}
get isActive():boolean {
return this.mode !== 'inactive';
}
ngOnChanges() {
if (_.isNil(this.table)) {
return;
@ -163,7 +166,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
this.tableState.columns
.values$()
.pipe(
filter(() => this.isHidden), // Take only when row is inserted
filter(() => this.isActive), // Take only when row is inserted
untilComponentDestroyed(this),
)
.subscribe(() => {
@ -194,7 +197,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
}
// Notify inline create service
this.wpInlineCreate.newInlineWorkPackage(wp);
this.wpInlineCreate.newInlineWorkPackageCreated.next(wp.id);
} else {
// Remove current row
this.table.editing.stopEditing('new');
@ -209,6 +212,19 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
return false;
}
public handleReferenceClick() {
this.mode = 'reference';
return false;
}
public get referenceClass() {
return this.wpInlineCreate.referenceComponentClass;
}
public get hasReferenceClass() {
return !!this.referenceClass;
}
public addWorkPackageRow() {
this.wpCreate
.createNewWorkPackage(this.projectIdentifier)
@ -302,20 +318,23 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe
}
public showRow() {
return this.isHidden = false;
this.mode = 'inactive';
}
public hideRow() {
return this.isHidden = true;
this.mode = 'create';
}
public get colspan():number {
return this.wpTableColumns.columnCount + 1;
}
public get isAllowed():boolean {
return this.authorisationService.can('work_packages', 'createWorkPackage') ||
this.authorisationService.can('work_package', 'addChild');
public get canReference():boolean {
return this.hasReferenceClass && this.wpInlineCreate.canReference;
}
public get canAdd():boolean {
return this.wpInlineCreate.canAdd;
}
}

@ -26,71 +26,59 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {
Component,
ElementRef, HostListener,
Inject, Injectable,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit
} from '@angular/core';
import {AuthorisationService} from 'core-app/modules/common/model-auth/model-auth.service';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {filter, takeUntil} from 'rxjs/operators';
import {Injectable, OnDestroy} from '@angular/core';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {TableRowEditContext} from '../wp-edit-form/table-row-edit-context';
import {WorkPackageChangeset} from '../wp-edit-form/work-package-changeset';
import {WorkPackageEditForm} from '../wp-edit-form/work-package-edit-form';
import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-service';
import {WorkPackageFilterValues} from '../wp-edit-form/work-package-filter-values';
import {TimelineRowBuilder} from '../wp-fast-table/builders/timeline/timeline-row-builder';
import {onClickOrEnter} from '../wp-fast-table/handlers/click-or-enter-handler';
import {WorkPackageTableColumnsService} from '../wp-fast-table/state/wp-table-columns.service';
import {WorkPackageTableFiltersService} from '../wp-fast-table/state/wp-table-filters.service';
import {WorkPackageTable} from '../wp-fast-table/wp-fast-table';
import {WorkPackageCreateService} from '../wp-new/wp-create.service';
import {
inlineCreateCancelClassName,
InlineCreateRowBuilder,
inlineCreateRowClassName
} from './inline-create-row-builder';
import {TableState} from 'core-components/wp-table/table-state/table-state';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {IWorkPackageEditingServiceToken} from "../wp-edit-form/work-package-editing.service.interface";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {CurrentUserService} from "core-components/user/current-user.service";
import {Observable, Subject} from "rxjs";
import {Subject} from "rxjs";
import {ComponentType} from "@angular/cdk/portal";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service";
@Injectable()
export class WorkPackageInlineCreateService implements OnDestroy {
/** Allow callbacks to happen on newly created inline work packages */
protected _newInlineWorkPackage = new Subject<WorkPackageResource>();
constructor(protected readonly I18n:I18nService,
protected readonly authorisationService:AuthorisationService) {
}
/**
* Ensure hierarchical injected versions of this service correctly unregister
* A separate reference pane for the inline create component
*/
ngOnDestroy() {
this._newInlineWorkPackage.complete();
}
public readonly referenceComponentClass:ComponentType<any>|null = null;
/**
* Returns an observable that fires whenever a new INLINE work packages was created.
* A related work package for the inline create context
*/
public newInlineWorkPackageCreated$():Observable<WorkPackageResource> {
return this._newInlineWorkPackage.asObservable();
public referenceTarget:WorkPackageResource|null = null;
/**
* Reference button text
*/
public readonly buttonTexts = {
reference: '',
create: this.I18n.t('js.label_create_work_package'),
};
public get canAdd() {
return this.authorisationService.can('work_packages', 'createWorkPackage') ||
this.authorisationService.can('work_package', 'addChild');
}
public get canReference() {
return false;
}
/** Allow callbacks to happen on newly created inline work packages */
public newInlineWorkPackageCreated = new Subject<string>();
/** Allow callbacks to happen on newly created inline work packages */
public newInlineWorkPackageReferenced = new Subject<string>();
/**
* Notify of a new inline work package that was created
* @param wp Work package that got created
* Ensure hierarchical injected versions of this service correctly unregister
*/
public newInlineWorkPackage(wp:WorkPackageResource) {
this._newInlineWorkPackage.next(wp);
ngOnDestroy() {
this.newInlineWorkPackageCreated.complete();
this.newInlineWorkPackageReferenced.complete();
}
}

@ -0,0 +1,29 @@
<div class="loading-indicator--location"
data-indicator-name="relationAddChild">
<div class="v-align wp-relations-create--form wp-relations--child-form">
<div class="grid-content medium-10">
<wp-relations-autocomplete-upgraded
[workPackage]="workPackage"
(onWorkPackageIdSelected)="updateSelectedId($event)"
loadingPromiseName="relationAddChild"
filterCandidatesFor="children">
</wp-relations-autocomplete-upgraded>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
linkClass="wp-create-relation--save"
[isDisabled]="isDisabled"
(execute)="addExistingChild()"
aria-hidden="false">
<op-icon icon-classes="icon-checkmark" [icon-title]="text.save"></op-icon>
</accessible-by-keyboard>
&nbsp;
<accessible-by-keyboard
linkClass="wp-create-relation--cancel"
(execute)="cancel()"
aria-hidden="false">
<op-icon icon-classes="icon-remove" [icon-title]="text.abort"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>

@ -0,0 +1,96 @@
// -- 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} from '@angular/core';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {WorkPackageInlineCreateComponent} from "core-components/wp-inline-create/wp-inline-create.component";
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {WorkPackageRelationsHierarchyService} from "core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service";
@Component({
templateUrl: './wp-inline-add-existing-child.component.html'
})
export class WorkPackageInlineAddExistingChildComponent {
public selectedWpId:string;
public isDisabled = false;
public text = {
save: this.I18n.t('js.relation_buttons.save'),
abort: this.I18n.t('js.relation_buttons.abort'),
addNewChild: this.I18n.t('js.relation_buttons.add_new_child'),
addExistingChild: this.I18n.t('js.relation_buttons.add_existing_child')
};
constructor(protected readonly parent:WorkPackageInlineCreateComponent,
protected readonly wpInlineCreate:WorkPackageInlineCreateService,
protected wpCacheService:WorkPackageCacheService,
protected wpRelations:WorkPackageRelationsService,
protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
protected wpNotificationsService:WorkPackageNotificationService,
protected readonly I18n:I18nService) {
}
public addExistingChild() {
if (_.isNil(this.selectedWpId)) {
return;
}
const newChildId = this.selectedWpId;
this.isDisabled = true;
this.wpRelationsHierarchyService
.addExistingChildWp(this.workPackage, newChildId)
.then(() => {
this.wpCacheService.loadWorkPackage(this.workPackage.id, true);
this.isDisabled = false;
this.wpInlineCreate.newInlineWorkPackageReferenced.next(newChildId);
this.cancel();
})
.catch((err:any) => {
this.wpNotificationsService.handleRawError(err, this.workPackage);
this.isDisabled = false;
this.cancel();
});
}
public updateSelectedId(workPackageId:string) {
this.selectedWpId = workPackageId;
}
public get workPackage() {
return this.wpInlineCreate.referenceTarget!;
}
public cancel() {
this.parent.resetRow();
}
}

@ -0,0 +1,78 @@
// -- 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 {Injectable, OnDestroy} from '@angular/core';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {Subject} from "rxjs";
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {WorkPackageInlineAddExistingChildComponent} from "core-components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.component";
@Injectable()
export class WorkPackageInlineAddExistingChildService extends WorkPackageInlineCreateService implements OnDestroy {
/**
* A separate reference pane for the inline create component
*/
public readonly referenceComponentClass = WorkPackageInlineAddExistingChildComponent;
/**
* A related work package for the inline create context
*/
public referenceTarget:WorkPackageResource|null = null;
public get canAdd() {
return !!(this.referenceTarget && this.referenceTarget.addChild);
}
public get canReference() {
return !!(this.referenceTarget && !this.referenceTarget.isMilestone && this.referenceTarget.changeParent);
}
/**
* Reference button text
*/
public readonly buttonTexts = {
reference: this.I18n.t('js.relation_buttons.add_existing_child'),
create: this.I18n.t('js.relation_buttons.add_new_child')
};
/** Allow callbacks to happen on newly created inline work packages */
public newInlineWorkPackageCreated = new Subject<string>();
/** Allow callbacks to happen on newly created inline work packages */
public newInlineWorkPackageReferenced = new Subject<string>();
/**
* Ensure hierarchical injected versions of this service correctly unregister
*/
ngOnDestroy() {
this.newInlineWorkPackageCreated.complete();
this.newInlineWorkPackageReferenced.complete();
}
}

@ -1,60 +0,0 @@
<div>
<div class="wp-relations-create-button -full-width"
*ngIf="!showRelationsCreateForm">
<div class="grid-block">
<div class="grid-content collapse hide-when-print"
*ngIf="canAddChildren">
<a class="wp-inline-create--add-link relation-create"
(click)="createNewChildWorkPackage()"
role="button"
id="hierarchy--add-new-child">
<op-icon icon-classes="icon icon-add"></op-icon>
<span [textContent]="text.addNewChild"></span>
</a>
</div>
<div class="grid-content collapse hide-when-print"
*ngIf="canLinkChildren">
<a class="wp-inline-create--add-link relation-create -focus-after-save"
(click)="toggleRelationsCreateForm()"
role="button"
id="hierarchy--add-exisiting-child">
<op-icon icon-classes="icon icon-add"></op-icon>
<span [textContent]="text.addExistingChild"></span>
</a>
</div>
</div>
</div>
<div class="loading-indicator--location"
data-indicator-name="relationAddChild">
<div class="v-align wp-relations-create--form wp-relations--child-form"
*ngIf="showRelationsCreateForm">
<div class="grid-content medium-10">
<wp-relations-autocomplete-upgraded
[workPackage]="workPackage"
(onWorkPackageIdSelected)="updateSelectedId($event)"
loadingPromiseName="relationAddChild"
filterCandidatesFor="children">
</wp-relations-autocomplete-upgraded>
</div>
<div class="grid-content medium-2 collapse wp-relations-controls-section relation-row">
<accessible-by-keyboard
linkClass="wp-create-relation--save"
[isDisabled]="isDisabled"
(execute)="addExistingChild()"
aria-hidden="false">
<op-icon icon-classes="icon-checkmark" [icon-title]="text.save"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard
linkClass="wp-create-relation--cancel"
(execute)="toggleRelationsCreateForm()"
aria-hidden="false">
<op-icon icon-classes="icon-remove" [icon-title]="text.abort"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>
</div>

@ -1,88 +0,0 @@
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, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
@Component({
selector: 'wp-relation-add-child',
templateUrl: './wp-relation-add-child.html'
})
export class WpRelationAddChildComponent implements OnInit {
@Input() public workPackage:WorkPackageResource;
@Output() public onAdded = new EventEmitter<string>();
public showRelationsCreateForm: boolean = false;
public isDisabled = false;
public canAddChildren:boolean = false;
public canLinkChildren:boolean = false;
public selectedWpId:string|null = null;
public text = {
save: this.I18n.t('js.relation_buttons.save'),
abort: this.I18n.t('js.relation_buttons.abort'),
addNewChild: this.I18n.t('js.relation_buttons.add_new_child'),
addExistingChild: this.I18n.t('js.relation_buttons.add_existing_child')
};
private $element:JQuery;
constructor(readonly I18n:I18nService,
readonly elementRef:ElementRef,
protected wpRelations:WorkPackageRelationsService,
protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpCacheService:WorkPackageCacheService) {
}
ngOnInit() {
this.$element = jQuery(this.elementRef.nativeElement);
this.canAddChildren = !!this.workPackage.addChild;
this.canLinkChildren = !!this.workPackage.changeParent;
}
public updateSelectedId(workPackageId:string) {
this.selectedWpId = workPackageId;
}
public addExistingChild() {
if (_.isNil(this.selectedWpId)) {
return;
}
const newChildId = this.selectedWpId;
this.isDisabled = true;
this.wpRelationsHierarchyService
.addExistingChildWp(this.workPackage, newChildId)
.then(() => {
this.wpCacheService.loadWorkPackage(this.workPackage.id, true);
this.isDisabled = false;
this.onAdded.emit(newChildId);
this.toggleRelationsCreateForm();
})
.catch(err => {
this.wpNotificationsService.handleRawError(err, this.workPackage);
this.isDisabled = false;
this.toggleRelationsCreateForm();
});
}
public createNewChildWorkPackage() {
this.wpRelationsHierarchyService.addNewChildWp(this.workPackage);
}
public toggleRelationsCreateForm() {
this.showRelationsCreateForm = !this.showRelationsCreateForm;
setTimeout(() => {
if (!this.showRelationsCreateForm) {
this.selectedWpId = null;
this.$element.find('.-focus-after-save').first().focus();
}
});
}
}

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
@ -35,12 +35,18 @@ import {WorkPackageRelationsHierarchyService} from 'core-components/wp-relations
import {WorkPackageEmbeddedTableComponent} from 'core-components/wp-table/embedded/wp-embedded-table.component';
import {OpUnlinkTableAction} from 'core-components/wp-table/table-actions/actions/unlink-table-action';
import {OpTableActionFactory} from 'core-components/wp-table/table-actions/table-action';
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {WorkPackageInlineAddExistingChildService} from "core-components/wp-relations/wp-relation-add-child/wp-inline-add-existing-child.service";
@Component({
selector: 'wp-children-query',
templateUrl: './wp-children-query.html'
templateUrl: './wp-children-query.html',
providers: [
{ provide: WorkPackageInlineCreateService, useClass: WorkPackageInlineAddExistingChildService }
]
})
export class WorkPackageChildrenQueryComponent implements OnInit {
export class WorkPackageChildrenQueryComponent implements OnInit, OnDestroy {
@Input() public workPackage:WorkPackageResource;
@Input() public query:any;
@Input() public addExistingChildEnabled:boolean = false;
@ -66,11 +72,20 @@ export class WorkPackageChildrenQueryComponent implements OnInit {
constructor(protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
protected PathHelper:PathHelperService,
protected wpInlineCreate:WorkPackageInlineCreateService,
protected queryUrlParamsHelper:UrlParamsHelperService,
readonly I18n:I18nService) {
}
ngOnInit() {
// Set reference target and reference class
this.wpInlineCreate.referenceTarget = this.workPackage;
// Wire the successful saving of a new addition to refreshing the embedded table
this.wpInlineCreate.newInlineWorkPackageReferenced
.pipe(untilComponentDestroyed(this))
.subscribe(() => this.refreshTable());
this.canHaveChildren = !this.workPackage.isMilestone;
this.canModifyHierarchy = !!this.workPackage.changeParent;
@ -82,6 +97,10 @@ export class WorkPackageChildrenQueryComponent implements OnInit {
}
}
ngOnDestroy() {
// Nothing to do
}
public refreshTable() {
this.childrenEmbeddedTable.refresh();
}

@ -11,8 +11,3 @@
projectIdentifier: workPackage.project.idFromLink,
projectContext: false }" >
</wp-embedded-table>
<wp-relation-add-child
[workPackage]="workPackage"
(onAdded)="refreshTable()"
*ngIf="canModifyHierarchy && canHaveChildren && addExistingChildEnabled"></wp-relation-add-child>

@ -70,7 +70,7 @@ export class WpRelationsAutocompleteComponent implements OnInit {
input.autocomplete({
delay: 250,
autoFocus: false, // Accessibility!
appendTo: '.detail-panel--autocomplete-target',
appendTo: '#content',
classes: {
'ui-autocomplete': 'wp-relations-autocomplete--results'
},

@ -11,7 +11,7 @@ export interface GlobalI18n {
@Injectable()
export class I18nService {
private _i18n:GlobalI18n
private _i18n:GlobalI18n;
constructor() {
this._i18n = (window as any).I18n;

@ -245,7 +245,7 @@ module API
link :addChild,
cache_if: -> { current_user_allowed_to(:add_work_packages, context: represented.project) } do
next if represented.new_record?
next if represented.milestone? || represented.new_record?
{
href: api_v3_paths.work_packages_by_project(represented.project.identifier),
method: :post,

@ -46,14 +46,14 @@ shared_examples 'work package relations tab', js: true, selenium: true do
##
# Add child #1
find('.wp-inline-create--add-link',
find('.wp-inline-create--reference-link',
text: I18n.t('js.relation_buttons.add_existing_child')).click
relations.add_existing_child(child)
##
# Add child #2
find('.wp-inline-create--add-link',
find('.wp-inline-create--reference-link',
text: I18n.t('js.relation_buttons.add_existing_child')).click
relations.add_existing_child(child2)
@ -172,7 +172,7 @@ shared_examples 'work package relations tab', js: true, selenium: true do
##
# Add child
find('.wp-inline-create--add-link',
find('.wp-inline-create--reference-link',
text: I18n.t('js.relation_buttons.add_existing_child')).click
relations.add_existing_child(child)

@ -1,176 +0,0 @@
#-- 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.
#++
require 'spec_helper'
RSpec.feature 'Work package create children', js: true, selenium: true do
let(:tabs) { ::Components::WorkPackages::Tabs.new(original_work_package) }
let(:relations_tab) { find('.tabrow li', text: 'RELATIONS') }
let(:user) do
FactoryBot.create(:user,
member_in_project: project,
member_through_role: create_role)
end
let(:work_flow) do
FactoryBot.create(:workflow,
role: create_role,
type_id: original_work_package.type_id,
old_status: original_work_package.status,
new_status: FactoryBot.create(:status))
end
let(:create_role) do
FactoryBot.create(:role,
permissions: [:view_work_packages,
:add_work_packages,
:edit_work_packages,
:manage_subtasks])
end
let(:project) { FactoryBot.create(:project) }
let(:original_work_package) do
FactoryBot.build(:work_package,
project: project,
assigned_to: assignee,
responsible: responsible,
fixed_version: version,
priority: default_priority,
author: author,
status: default_status)
end
let(:default_priority) do
FactoryBot.build(:default_priority)
end
let(:default_status) do
FactoryBot.build(:default_status)
end
let(:role) { FactoryBot.build(:role, permissions: [:view_work_packages]) }
let(:assignee) do
FactoryBot.build(:user,
firstname: 'An',
lastname: 'assignee',
member_in_project: project,
member_through_role: role)
end
let(:responsible) do
FactoryBot.build(:user,
firstname: 'The',
lastname: 'responsible',
member_in_project: project,
member_through_role: role)
end
let(:author) do
FactoryBot.build(:user,
firstname: 'The',
lastname: 'author',
member_in_project: project,
member_through_role: role)
end
let(:version) do
FactoryBot.build(:version,
project: project)
end
before do
login_as(user)
allow(user.pref).to receive(:warn_on_leaving_unsaved?).and_return(false)
original_work_package.save!
work_flow.save!
end
scenario 'on fullscreen page' do
original_work_package_page = Pages::FullWorkPackage.new(original_work_package)
child_work_package_page = original_work_package_page.add_child
expect_angular_frontend_initialized
type_field = child_work_package_page.edit_field :type
type_field.expect_active!
expect(type_field.input_element).to have_selector('option[selected]', text: 'Please select')
child_work_package_page.expect_current_path
child_work_package_page.update_attributes Subject: 'Child work package',
Type: 'None'
expect(type_field.input_element).to have_selector('option[selected]', text: 'None')
child_work_package_page.save!
expect(page).to have_selector('.notification-box--content',
text: I18n.t('js.notice_successful_create'))
# Relations counter in full view should equal 1
tabs.expect_counter(relations_tab, 1)
child_work_package = WorkPackage.order(created_at: 'desc').first
expect(child_work_package).to_not eql original_work_package
child_work_package_page = Pages::FullWorkPackage.new(child_work_package, project)
child_work_package_page.ensure_page_loaded
child_work_package_page.expect_subject
child_work_package_page.expect_current_path
child_work_package_page.expect_parent(original_work_package)
end
scenario 'on split screen page' do
original_work_package_page = Pages::SplitWorkPackage.new(original_work_package, project)
child_work_package_page = original_work_package_page.add_child
expect_angular_frontend_initialized
type_field = child_work_package_page.edit_field :type
expect(type_field.input_element).to have_selector('option[selected]', text: 'Please select')
child_work_package_page.expect_current_path
child_work_package_page.update_attributes Subject: 'Child work package',
Type: 'None'
expect(type_field.input_element).to have_selector('option[selected]', text: 'None')
child_work_package_page.save!
expect(page).to have_selector('.notification-box--content',
text: I18n.t('js.notice_successful_create'))
# # Relations counter in split view should equal 1
tabs.expect_counter(relations_tab, 1)
child_work_package = WorkPackage.order(created_at: 'desc').first
expect(child_work_package).to_not eql original_work_package
child_work_package_page = Pages::SplitWorkPackage.new(child_work_package, project)
child_work_package_page.ensure_page_loaded
child_work_package_page.expect_subject
child_work_package_page.expect_current_path
child_work_package_page.expect_parent(original_work_package)
end
end

@ -131,7 +131,7 @@ module Components
autocomplete = container.find(".wp-relations--autocomplete")
select_autocomplete autocomplete,
query: query,
results_selector: '.detail-panel--relations .wp-relations-autocomplete--results',
results_selector: '.wp-relations-autocomplete--results',
select_text: work_package.id
container.find('.wp-create-relation--save').click

Loading…
Cancel
Save