From e41e26e2a05c29867e1f4a2b75d068d6489b00a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 24 Oct 2018 08:26:44 +0200 Subject: [PATCH] Create an inline create service to listen to inline created WPs --- frontend/src/app/angular4-modules.ts | 6 + .../wp-inline-create.component.ts | 195 ++++++++++++------ .../wp-inline-create.service.ts | 96 +++++++++ 3 files changed, 232 insertions(+), 65 deletions(-) create mode 100644 frontend/src/app/components/wp-inline-create/wp-inline-create.service.ts diff --git a/frontend/src/app/angular4-modules.ts b/frontend/src/app/angular4-modules.ts index f22c6065ae..2aefddc8e6 100644 --- a/frontend/src/app/angular4-modules.ts +++ b/frontend/src/app/angular4-modules.ts @@ -219,6 +219,7 @@ import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table import {ChartsModule} from "ng2-charts"; import {WorkPackageEmbeddedGraphComponent} from "core-components/wp-table/embedded/wp-embedded-graph.component"; 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"; @NgModule({ @@ -298,6 +299,11 @@ import {WorkPackageCommentFieldComponent} from "core-components/work-packages/wo // Provide both serves with tokens to avoid tight dependency cycles { provide: IWorkPackageCreateServiceToken, useClass: WorkPackageCreateService }, { provide: IWorkPackageEditingServiceToken, useClass: WorkPackageEditingService }, + + // 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, + OpTableActionsService, CurrentProjectService, FirstRouteService, diff --git a/frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts b/frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts index bd35dcf18a..4aff8dced6 100644 --- a/frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts +++ b/frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts @@ -58,12 +58,13 @@ import { inlineCreateRowClassName } from './inline-create-row-builder'; import {TableState} from 'core-components/wp-table/table-state/table-state'; -import {componentDestroyed} from 'ng2-rx-componentdestroyed'; +import {componentDestroyed, 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"; import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface"; import {CurrentUserService} from "core-components/user/current-user.service"; +import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service"; @Component({ selector: '[wpInlineCreate]', @@ -94,19 +95,19 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe private $element:JQuery; - constructor(readonly elementRef:ElementRef, - readonly injector:Injector, - readonly FocusHelper:FocusHelperService, - readonly I18n:I18nService, - readonly tableState:TableState, - readonly wpCacheService:WorkPackageCacheService, - readonly currentUser:CurrentUserService, - @Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService, - @Inject(IWorkPackageCreateServiceToken) protected wpCreate:WorkPackageCreateService, - readonly wpTableColumns:WorkPackageTableColumnsService, - readonly wpTableFilters:WorkPackageTableFiltersService, - readonly wpTableFocus:WorkPackageTableFocusService, - readonly authorisationService:AuthorisationService) { + constructor(protected readonly elementRef:ElementRef, + protected readonly injector:Injector, + protected readonly FocusHelper:FocusHelperService, + protected readonly I18n:I18nService, + protected readonly tableState:TableState, + protected readonly wpCacheService:WorkPackageCacheService, + protected readonly currentUser:CurrentUserService, + @Inject(IWorkPackageEditingServiceToken) protected readonly wpEditing:WorkPackageEditingService, + @Inject(IWorkPackageCreateServiceToken) protected readonly wpCreate:WorkPackageCreateService, + protected readonly wpInlineCreate:WorkPackageInlineCreateService, + protected readonly wpTableColumns:WorkPackageTableColumnsService, + protected readonly wpTableFocus:WorkPackageTableFocusService, + protected readonly authorisationService:AuthorisationService) { } ngOnDestroy() { @@ -129,11 +130,59 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe const container = jQuery(this.table.timelineBody); container.addClass('-inline-create-mirror'); - // Remove temporary rows on creation of new work package - this.wpCreate.onNewWorkPackage() + // Register callback on newly created work packages + this.registerCreationCallback(); + + // Watch on this scope when the columns change and refresh this row + this.refreshOnColumnChanges(); + + // Cancel edition of current new row + this.registerCancelHandler(); + } + + /** + * Reset the inline creation row on the cancel button, + * which is dynamically inserted into the action row by the inline create renderer. + */ + private registerCancelHandler() { + this.$element.on('click keydown', `.${inlineCreateCancelClassName}`, (evt) => { + onClickOrEnter(evt, () => { + this.resetRow(); + }); + + evt.stopImmediatePropagation(); + return false; + }); + } + + /** + * Since the table is refreshed imperatively whenever columns are changed, + * we need to manually ensure the inline create row gets refreshed as well. + */ + private refreshOnColumnChanges() { + this.tableState.columns + .values$() .pipe( - takeUntil(componentDestroyed(this)) + filter(() => this.isHidden), // Take only when row is inserted + untilComponentDestroyed(this), ) + .subscribe(() => { + const rowElement = this.$element.find(`.${inlineCreateRowClassName}`); + + if (rowElement.length && this.currentWorkPackage) { + this.rowBuilder.refreshRow(this.currentWorkPackage, rowElement); + } + }); + } + + /** + * Listen to newly created work packages to detect whether the WP is the one we created, + * and properly reset inline create in this case + */ + private registerCreationCallback() { + this.wpCreate + .onNewWorkPackage() + .pipe(untilComponentDestroyed(this)) .subscribe((wp:WorkPackageResource) => { if (this.currentWorkPackage && this.currentWorkPackage === wp) { // Remove row and focus @@ -143,6 +192,9 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe if (!this.table.configuration.isEmbedded) { this.wpTableFocus.updateFocus(wp.id); } + + // Notify inline create service + this.wpInlineCreate.newInlineWorkPackage(wp); } else { // Remove current row this.table.editing.stopEditing('new'); @@ -150,30 +202,6 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe this.showRow(); } }); - - // Watch on this scope when the columns change and refresh this row - this.tableState.columns.values$() - .pipe( - filter(() => this.isHidden), // Take only when row is inserted - takeUntil(componentDestroyed(this)) - ) - .subscribe(() => { - const rowElement = this.$element.find(`.${inlineCreateRowClassName}`); - - if (rowElement.length && this.currentWorkPackage) { - this.rowBuilder.refreshRow(this.currentWorkPackage, rowElement); - } - }); - - // Cancel edition of current new row - this.$element.on('click keydown', `.${inlineCreateCancelClassName}`, (evt) => { - onClickOrEnter(evt, () => { - this.resetRow(); - }); - - evt.stopImmediatePropagation(); - return false; - }); } public handleAddRowClick() { @@ -182,41 +210,77 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe } public addWorkPackageRow() { - this.wpCreate.createNewWorkPackage(this.projectIdentifier).then((changeset:WorkPackageChangeset) => { + this.wpCreate + .createNewWorkPackage(this.projectIdentifier) + .then((changeset:WorkPackageChangeset) => { if (!changeset) { throw 'No new work package was created'; } const wp = this.currentWorkPackage = changeset.workPackage; + this.applyDefaultsAndInsert(changeset, wp); + }); + } + + /** + * Apply values to the work package from the current set of filters + * + * @param changeset + * @param wp + */ + private applyDefaultsAndInsert(changeset:WorkPackageChangeset, wp:WorkPackageResource) { + let promise:Promise; - // Apply filter values + if (this.tableState.query.hasValue()) { const filter = new WorkPackageFilterValues(this.currentUser, changeset, this.tableState.query.value!.filters); - filter.applyDefaultsFromFilters().then(() => { - this.wpEditing.updateValue('new', changeset); - this.wpCacheService.updateWorkPackage(this.currentWorkPackage!); - - // Set editing context to table - const context = new TableRowEditContext( - this.table, - this.injector, - wp.id, - this.rowBuilder.classIdentifier(wp) - ); - - this.workPackageEditForm = WorkPackageEditForm.createInContext(this.injector, context, wp, false); - this.workPackageEditForm.changeset.clear(); - - const row = this.rowBuilder.buildNew(wp, this.workPackageEditForm); - this.$element.append(row); - - setTimeout(() => { - this.workPackageEditForm!.activateMissingFields(); - this.hideRow(); - }); + promise = filter.applyDefaultsFromFilters(); + } else { + promise = Promise.resolve(); + } + + promise.then(() => { + // Update the changeset with any added filtered values + this.wpEditing.updateValue('new', changeset); + this.wpCacheService.updateWorkPackage(this.currentWorkPackage!); + + // Actually render the row + const form = this.workPackageEditForm = this.renderInlineCreateRow(wp); + + setTimeout(() => { + // Activate any required fields + form.activateMissingFields(); + + // Hide the button row + this.hideRow(); }); }); } + /** + * Actually render the row manually + * in the same fashion as all rows in the table are rendered. + * + * @param wp Work package to be rendered + * @returns The work package form of the row + */ + private renderInlineCreateRow(wp:WorkPackageResource):WorkPackageEditForm { + // Set editing context to table + const context = new TableRowEditContext( + this.table, + this.injector, + wp.id, + this.rowBuilder.classIdentifier(wp) + ); + + const form = WorkPackageEditForm.createInContext(this.injector, context, wp, false); + form.changeset.clear(); + + const row = this.rowBuilder.buildNew(wp, form); + this.$element.append(row); + + return form; + } + /** * Reset the new work package row and refocus on the button */ @@ -253,4 +317,5 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe return this.authorisationService.can('work_packages', 'createWorkPackage') || this.authorisationService.can('work_package', 'addChild'); } + } diff --git a/frontend/src/app/components/wp-inline-create/wp-inline-create.service.ts b/frontend/src/app/components/wp-inline-create/wp-inline-create.service.ts new file mode 100644 index 0000000000..02fe6bbe95 --- /dev/null +++ b/frontend/src/app/components/wp-inline-create/wp-inline-create.service.ts @@ -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, + 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 {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"; + +@Injectable() +export class WorkPackageInlineCreateService implements OnDestroy { + + /** Allow callbacks to happen on newly created inline work packages */ + protected _newInlineWorkPackage = new Subject(); + + /** + * Ensure hierarchical injected versions of this service correctly unregister + */ + ngOnDestroy() { + this._newInlineWorkPackage.complete(); + } + + /** + * Returns an observable that fires whenever a new INLINE work packages was created. + */ + public newInlineWorkPackageCreated$():Observable { + return this._newInlineWorkPackage.asObservable(); + } + + /** + * Notify of a new inline work package that was created + * @param wp Work package that got created + */ + public newInlineWorkPackage(wp:WorkPackageResource) { + this._newInlineWorkPackage.next(wp); + } +}