* Extract functionality to add versions from within an autocompleter to its own component.

* Create generic component for inline create autocompleter which handles the HTML and fires events.
* Use dynamic component in the add-modal of boards depending on the type
pull/7317/head
Henriette Dinger 6 years ago
parent 4e1104cbd6
commit 04513e5b12
  1. 3
      config/locales/js-en.yml
  2. 36
      frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts
  3. 30
      frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html
  4. 14
      frontend/src/app/modules/boards/board/board-actions/board-action.service.ts
  5. 12
      frontend/src/app/modules/boards/board/board-actions/status-action.service.ts
  6. 38
      frontend/src/app/modules/boards/board/board-actions/version-action.service.ts
  7. 108
      frontend/src/app/modules/common/autocomplete/create-autocompleter.component.ts
  8. 101
      frontend/src/app/modules/common/autocomplete/version-autocompleter.component.ts
  9. 19
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  10. 1
      modules/boards/spec/features/support/board_page.rb

@ -56,8 +56,6 @@ en:
lists:
delete: 'Delete list'
edit_version: 'Edit version'
add:
create_new: 'Create new'
card:
add_new: 'Add new card'
@ -244,6 +242,7 @@ en:
label_board_locked: "Locked"
label_board_plural: "Boards"
label_board_sticky: "Sticky"
label_create_new: "Create new"
label_create_work_package: "Create new work package"
label_created_by: "Created by"
label_date: "Date"

@ -28,10 +28,10 @@
import {OpModalComponent} from "core-components/op-modals/op-modal.component";
import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
import {ChangeDetectorRef, Component, ElementRef, Inject, OnInit, ViewChild} from "@angular/core";
import {ChangeDetectorRef, Component, ElementRef, Inject, OnInit} from "@angular/core";
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {Board, BoardType} from "core-app/modules/boards/board/board";
import {Board} from "core-app/modules/boards/board/board";
import {StateService} from "@uirouter/core";
import {BoardService} from "core-app/modules/boards/board/board.service";
import {BoardCacheService} from "core-app/modules/boards/board/board-cache.service";
@ -39,17 +39,12 @@ import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {BoardActionsRegistryService} from "core-app/modules/boards/board/board-actions/board-actions-registry.service";
import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {BoardListsService} from "core-app/modules/boards/board/board-list/board-lists.service";
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
import {NgSelectComponent} from "@ng-select/ng-select/dist";
@Component({
templateUrl: './add-list-modal.html'
})
export class AddListModalComponent extends OpModalComponent implements OnInit {
@ViewChild('addActionAttributeSelect') addAutoCompleter:NgSelectComponent;
@ViewChild('actionAttributeSelect') autoCompleter:NgSelectComponent;
public showClose:boolean;
public confirmed = false;
@ -71,9 +66,6 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
public trackByHref = AngularTrackingHelpers.trackByHref;
/* Is it allowed to add new action attributes from within the modal */
public createAllowed:boolean;
/* Do not close on outside click (because the select option are appended to the body */
public closeOnOutsideClick = false;
@ -90,8 +82,11 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
action_board_text: this.I18n.t('js.boards.board_type.action_text'),
select_attribute: this.I18n.t('js.boards.board_type.select_attribute'),
placeholder: this.I18n.t('js.placeholders.selection'),
};
add_new_action: this.I18n.t('js.boards.add.create_new'),
public referenceOutputs = {
onCreate: (value:HalResource) => this.onNewActionCreated(value),
onChange: (value:HalResource) => this.onModelChange(value)
};
constructor(readonly elementRef:ElementRef,
@ -118,15 +113,6 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
.then(available => {
this.availableValues = available;
});
this.actionService.canCreateNewActionElements().then((val) => {
this.createAllowed = val;
setTimeout(() => {
this.createAllowed ? this.addAutoCompleter.focus() : this.autoCompleter.focus();
});
});
}
onModelChange(element:HalResource) {
@ -144,13 +130,13 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
});
}
createNewElement(name:string) {
this.actionService.createNewActionElement(name).then((newElement) => {
if (newElement) {
this.selectedAttribute = newElement;
onNewActionCreated(newValue:HalResource) {
this.selectedAttribute = newValue;
this.create();
}
});
autocompleterComponent() {
return this.actionService.autocompleterComponent();
}
}

@ -17,30 +17,12 @@
<div class="form--field">
<div class="form--field-container">
<div class="form--select-container">
<label class="form--label" for="new_list_action_select" [textContent]="actionService.localizedName"></label>
<ng-select *ngIf="createAllowed"
#addActionAttributeSelect
name="new_list_action_select"
class="new-list--action-select"
[items]="availableValues"
[addTag]="createNewElement.bind(this)"
(change)="onModelChange($event)"
bindLabel="name"
appendTo="body">
<ng-template ng-tag-tmp let-search="searchTerm">
<b [textContent]="text.add_new_action"></b>: {{search}}
</ng-template>
</ng-select>
<ng-select *ngIf="!createAllowed"
#actionAttributeSelect
name="new_list_action_select"
class="new-list--action-select"
[items]="availableValues"
(change)="onModelChange($event)"
bindLabel="name"
appendTo="body">
</ng-select>
<label class="form--label" [textContent]="actionService.localizedName"></label>
<ndc-dynamic [ndcDynamicComponent]="autocompleterComponent()"
[ndcDynamicInputs]="{ availableValues: availableValues }"
[ndcDynamicOutputs]="referenceOutputs"
[ndcDynamicAttributes]="{ class: 'new-list--action-select' }">
</ndc-dynamic>
</div>
</div>
</div>

@ -1,6 +1,8 @@
import {Board} from "core-app/modules/boards/board/board";
import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {Component} from "@angular/compiler/src/core";
import {ComponentType} from "@angular/cdk/portal";
export interface BoardActionService {
@ -39,15 +41,9 @@ export interface BoardActionService {
*/
getAdditionalListMenuItems(actionAttributeValue:HalResource):Promise<any>;
/*
* Whether it is allowed to add new action entries (e.g. a new version)
* @returns {boolean}
*/
canCreateNewActionElements():Promise<boolean>;
/**
* Creates a new action entry (e.g. a new version) to be selectable as a list
* @returns {any}
* Get the specific component for the autocompleter (e.g versionAutocompleter)
* @returns {Component}
*/
createNewActionElement(name:string):Promise<HalResource|void>;
autocompleterComponent():ComponentType<any>;
}

@ -8,6 +8,7 @@ import {BoardActionService} from "core-app/modules/boards/board/board-actions/bo
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder";
import {CreateAutocompleterComponent} from "core-app/modules/common/autocomplete/create-autocompleter.component";
@Injectable()
export class BoardStatusActionService implements BoardActionService {
@ -85,12 +86,12 @@ export class BoardStatusActionService implements BoardActionService {
);
}
public canCreateNewActionElements():Promise<boolean> {
return Promise.resolve(false);
public getAdditionalListMenuItems(actionAttributeValue:HalResource):Promise<any> {
return Promise.resolve([]);
}
public createNewActionElement(name:string):Promise<HalResource|void> {
return Promise.reject('Endpoint to create status does not exist.');
public autocompleterComponent() {
return CreateAutocompleterComponent;
}
private getStatuses():Promise<StatusResource[]> {
@ -99,7 +100,4 @@ export class BoardStatusActionService implements BoardActionService {
.then(collection => collection.elements);
}
public getAdditionalListMenuItems(actionAttributeValue:HalResource):Promise<any> {
return Promise.resolve([]);
}
}

@ -10,6 +10,7 @@ import {VersionResource} from "core-app/modules/hal/resources/version-resource";
import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {VersionAutocompleterComponent} from "core-app/modules/common/autocomplete/version-autocompleter.component";
@Injectable()
export class BoardVersionActionService implements BoardActionService {
@ -88,27 +89,6 @@ export class BoardVersionActionService implements BoardActionService {
);
}
/**
* Checks for correct permissions
* (whether the current project is in the list of allowed values in the version create form)
* @returns {Promise<boolean>}
*/
public canCreateNewActionElements():Promise<boolean> {
var that = this;
return this.versionDm.emptyCreateForm(this.getVersionPayload('')).then((form) => {
return form.schema.definingProject.allowedValues.some((e:HalResource) => e.id === that.currentProject.id!);
});
}
/**
* Creates a new version with the given name
* @param {string} the name of the new version
* @returns {Promise<HalResource | void>}
*/
public createNewActionElement(name:string):Promise<HalResource|void> {
return this.versionDm.createVersion(this.getVersionPayload(name));
}
/**
* Adds an entry to the list menu to edit the version if allowed
* @param {HalResource} actionAttributeValue
@ -137,6 +117,10 @@ export class BoardVersionActionService implements BoardActionService {
}
}
public autocompleterComponent() {
return VersionAutocompleterComponent;
}
private getVersions():Promise<VersionResource[]> {
if (this.currentProject.id === null) {
return Promise.resolve([]);
@ -146,16 +130,4 @@ export class BoardVersionActionService implements BoardActionService {
.listForProject(this.currentProject.id)
.then(collection => collection.elements.filter(version => version.status === 'open'));
}
private getVersionPayload(name:string) {
let payload:any = {};
payload['name'] = name;
payload['_links'] = {
definingProject: {
href: this.pathHelper.api.v3.projects.id(this.currentProject.id!).path
}
};
return payload;
}
}

@ -0,0 +1,108 @@
// -- 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 {AfterViewInit, Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
import {NgSelectComponent} from "@ng-select/ng-select/dist";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
@Component({
template: `
<ng-select *ngIf="createAllowed"
#addActionAttributeSelect
[items]="availableValues"
[addTag]="createNewElement.bind(this)"
(change)="changeModel($event)"
bindLabel="name"
appendTo="body">
<ng-template ng-tag-tmp let-search="searchTerm">
<b [textContent]="text.add_new_action"></b>: {{search}}
</ng-template>
</ng-select>
<ng-select *ngIf="!createAllowed"
#actionAttributeSelect
[items]="availableValues"
(change)="changeModel($event)"
bindLabel="name"
appendTo="body">
</ng-select>
`,
selector: 'create-autocompleter'
})
export class CreateAutocompleterComponent implements AfterViewInit {
@ViewChild('addActionAttributeSelect') public addAutoCompleter:NgSelectComponent;
@ViewChild('actionAttributeSelect') public autoCompleter:NgSelectComponent;
@Input() public availableValues:any[];
@Input() public set createAllowed(val:boolean) {
this._createAllowed = val;
setTimeout(() => {
this.focusInputField();
});
};
@Output() public onCreate = new EventEmitter<string>();
@Output() public onChange = new EventEmitter<HalResource>();
private _createAllowed:boolean = false;
public text:any = {
add_new_action: this.I18n.t('js.label_create_new'),
};
constructor(readonly I18n:I18nService,
readonly currentProject:CurrentProjectService,
readonly pathHelper:PathHelperService) {
}
ngAfterViewInit() {
this.focusInputField();
}
public createNewElement(name:string) {
this.onCreate.emit(name);
}
public changeModel(element:HalResource) {
this.onChange.emit(element);
}
public get createAllowed() {
return this._createAllowed;
}
private focusInputField() {
this.createAllowed ? this.addAutoCompleter.focus() : this.autoCompleter.focus();
}
}
DynamicBootstrapper.register({ selector: 'add-attribute-autocompleter', cls: CreateAutocompleterComponent });

@ -0,0 +1,101 @@
// -- 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {VersionResource} from "core-app/modules/hal/resources/version-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
@Component({
template: `
<create-autocompleter [availableValues]="availableValues"
[createAllowed]="createAllowed"
(onCreate)="createNewVersion($event)"
(onChange)="onModelChanged($event)">
</create-autocompleter>
`,
selector: 'version-autocompleter'
})
export class VersionAutocompleterComponent implements OnInit {
@Input() public availableValues:any[];
@Input() public createAllowed:boolean;
@Output() public onCreate = new EventEmitter<VersionResource>();
@Output() public onChange = new EventEmitter<VersionResource>();
constructor(readonly currentProject:CurrentProjectService,
readonly pathHelper:PathHelperService,
readonly versionDm:VersionDmService) {
}
ngOnInit() {
this.canCreateNewActionElements().then((val) => {
this.createAllowed = val;
});
}
/**
* Checks for correct permissions
* (whether the current project is in the list of allowed values in the version create form)
* @returns {Promise<boolean>}
*/
public canCreateNewActionElements():Promise<boolean> {
var that = this;
return this.versionDm.emptyCreateForm(this.getVersionPayload('')).then((form) => {
return form.schema.definingProject.allowedValues.some((e:HalResource) => e.id === that.currentProject.id!);
});
}
public createNewVersion(name:string) {
this.versionDm.createVersion(this.getVersionPayload(name)).then((version) => {
this.onCreate.emit(version)
});
}
public onModelChanged(element:VersionResource) {
this.onChange.emit(element);
}
private getVersionPayload(name:string) {
let payload:any = {};
payload['name'] = name;
payload['_links'] = {
definingProject: {
href: this.pathHelper.api.v3.projects.id(this.currentProject.id!).path
}
};
return payload;
}
}
DynamicBootstrapper.register({ selector: 'version-autocompleter', cls: VersionAutocompleterComponent });

@ -157,6 +157,8 @@ import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-packag
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {OpenprojectBcfModule} from "core-app/modules/bcf/openproject-bcf.module";
import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component";
import {CreateAutocompleterComponent} from "core-app/modules/common/autocomplete/create-autocompleter.component";
import {VersionAutocompleterComponent} from "core-app/modules/common/autocomplete/version-autocompleter.component";
@NgModule({
imports: [
@ -175,7 +177,10 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
// Work package custom actions
//WpCustomActionsModule,
DynamicModule.withComponents([WorkPackageFormAttributeGroupComponent, WorkPackageChildrenQueryComponent])
DynamicModule.withComponents([WorkPackageFormAttributeGroupComponent,
WorkPackageChildrenQueryComponent,
VersionAutocompleterComponent,
CreateAutocompleterComponent])
],
providers: [
{
@ -363,6 +368,10 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
// Card view
WorkPackageCardViewComponent,
// Autocompleter
CreateAutocompleterComponent,
VersionAutocompleterComponent,
],
entryComponents: [
// Split view
@ -437,6 +446,10 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
// Card view
WorkPackageCardViewComponent,
// Autocompleter
CreateAutocompleterComponent,
VersionAutocompleterComponent,
],
exports: [
WorkPackagesTableController,
@ -447,6 +460,10 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
WorkPackageFilterButtonComponent,
WorkPackageFilterContainerComponent,
WorkPackageIsolatedQuerySpaceDirective,
CreateAutocompleterComponent,
VersionAutocompleterComponent,
DynamicModule,
]
})
export class OpenprojectWorkPackagesModule {

@ -310,6 +310,7 @@ module Pages
def open_and_fill_add_list_modal(name)
page.find('.boards-list--add-item').click
expect(page).to have_selector('.new-list--action-select input')
page.find('.op-modal--modal-container .new-list--action-select input').set(name)
end
end

Loading…
Cancel
Save