Consolidate edit fields with their components (#6688)
* Consolidate edit fields with their components Previously, we have edit field modules (typescript classes) and their component counterparts (projected into CDK portals). This was mainly due to the old AngularJS solution used one edit field component with a dynamic field for rendering, which is now no longer applicable. In Angular, we need a 1-to-1 mapping between component and template for edit fields. This PR thus treats `EditFieldComponents` as a subclass of `Field` so we don't need to build both. This issue became apparent in the CKEditor instance. * Better error handling to avoid JSON errors on submit * Restore onSubmit we need to use to update field values * Fix new spec by correctly rejecting save * Fix demo data seeder spec * Correctly read data from onSubmit handler before saving Otherwise, the value saved may not be the absolute recent value.pull/6776/head
parent
650608e798
commit
deb448f1fa
@ -0,0 +1,106 @@ |
|||||||
|
import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler"; |
||||||
|
import {ElementRef, Injector, OnInit} from "@angular/core"; |
||||||
|
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; |
||||||
|
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset"; |
||||||
|
import {IFieldSchema} from "core-app/modules/fields/field.base"; |
||||||
|
import {Subject} from "rxjs"; |
||||||
|
|
||||||
|
export abstract class WorkPackageCommentFieldHandler extends EditFieldHandler implements OnInit { |
||||||
|
public fieldName = 'comment'; |
||||||
|
public handler = this; |
||||||
|
public inEdit = false; |
||||||
|
public inEditMode = false; |
||||||
|
public inFlight = false; |
||||||
|
|
||||||
|
public changeset:WorkPackageChangeset; |
||||||
|
|
||||||
|
// Destroy events
|
||||||
|
public onDestroy = new Subject<void>(); |
||||||
|
|
||||||
|
constructor(protected elementRef:ElementRef, |
||||||
|
protected injector:Injector) { |
||||||
|
super(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Handle saving the comment |
||||||
|
*/ |
||||||
|
public abstract handleUserSubmit():Promise<any>; |
||||||
|
|
||||||
|
/** |
||||||
|
* Required HTML id for the edit field |
||||||
|
*/ |
||||||
|
public abstract get htmlId():string; |
||||||
|
|
||||||
|
/** |
||||||
|
* Required field label translation |
||||||
|
*/ |
||||||
|
public abstract get fieldLabel():string; |
||||||
|
|
||||||
|
public abstract get workPackage():WorkPackageResource; |
||||||
|
|
||||||
|
ngOnInit() { |
||||||
|
this.changeset = new WorkPackageChangeset(this.injector, this.workPackage); |
||||||
|
} |
||||||
|
|
||||||
|
public reset(withText:string = '') { |
||||||
|
if (withText.length > 0) { |
||||||
|
withText += '\n'; |
||||||
|
} |
||||||
|
|
||||||
|
this.changeset.setValue('comment', { raw: withText }); |
||||||
|
} |
||||||
|
|
||||||
|
public get schema():IFieldSchema { |
||||||
|
return { |
||||||
|
name: I18n.t('js.label_comment'), |
||||||
|
writable: true, |
||||||
|
required: false, |
||||||
|
type: '_comment', |
||||||
|
hasDefault: false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public get rawComment() { |
||||||
|
return _.get(this.commentValue, 'raw', ''); |
||||||
|
} |
||||||
|
|
||||||
|
public get commentValue() { |
||||||
|
return this.changeset.value('comment'); |
||||||
|
} |
||||||
|
|
||||||
|
public handleUserCancel() { |
||||||
|
this.deactivate(true); |
||||||
|
} |
||||||
|
|
||||||
|
public get active() { |
||||||
|
return this.inEdit; |
||||||
|
} |
||||||
|
|
||||||
|
public activate(withText?:string) { |
||||||
|
this.inEdit = true; |
||||||
|
this.reset(withText); |
||||||
|
} |
||||||
|
|
||||||
|
deactivate(focus:boolean):void { |
||||||
|
this.inEdit = false; |
||||||
|
this.onDestroy.next(); |
||||||
|
this.onDestroy.complete(); |
||||||
|
} |
||||||
|
|
||||||
|
focus():void { |
||||||
|
const trigger = this.elementRef.nativeElement.querySelector('.inplace-editing--trigger-container'); |
||||||
|
trigger && trigger.focus(); |
||||||
|
} |
||||||
|
|
||||||
|
handleUserKeydown(event:JQueryEventObject, onlyCancel?:boolean):void { |
||||||
|
} |
||||||
|
|
||||||
|
isChanged():boolean { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
stopPropagation(evt:JQueryEventObject):boolean { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
@ -1,86 +0,0 @@ |
|||||||
// -- copyright
|
|
||||||
// OpenProject is a project management system.
|
|
||||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License version 3.
|
|
||||||
//
|
|
||||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
||||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
||||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License
|
|
||||||
// as published by the Free Software Foundation; either version 2
|
|
||||||
// of the License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program; if not, write to the Free Software
|
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
//
|
|
||||||
// See doc/COPYRIGHT.rdoc for more details.
|
|
||||||
// ++
|
|
||||||
|
|
||||||
import {Field, IFieldSchema} from "core-app/modules/fields/field.base"; |
|
||||||
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset"; |
|
||||||
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; |
|
||||||
|
|
||||||
|
|
||||||
export class EditField extends Field { |
|
||||||
readonly component:typeof EditFieldComponent; |
|
||||||
|
|
||||||
constructor(public changeset:WorkPackageChangeset, |
|
||||||
public name:string, |
|
||||||
public schema:IFieldSchema) { |
|
||||||
super(changeset.workPackage as any, name, schema); |
|
||||||
this.initialize(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Called when the edit field is open and ready |
|
||||||
* @param {HTMLElement} container |
|
||||||
*/ |
|
||||||
public $onInit(container:HTMLElement) { |
|
||||||
} |
|
||||||
|
|
||||||
public onSubmit() { |
|
||||||
} |
|
||||||
|
|
||||||
public get inFlight() { |
|
||||||
return this.changeset.inFlight; |
|
||||||
} |
|
||||||
|
|
||||||
public get value() { |
|
||||||
return this.changeset.value(this.name); |
|
||||||
} |
|
||||||
|
|
||||||
public set value(value:any) { |
|
||||||
this.changeset.setValue(this.name, this.parseValue(value)); |
|
||||||
} |
|
||||||
|
|
||||||
public get placeholder() { |
|
||||||
if (this.name === 'subject') { |
|
||||||
return this.I18n.t('js.placeholders.subject'); |
|
||||||
} |
|
||||||
|
|
||||||
return ''; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Initialize the field after constructor was called. |
|
||||||
*/ |
|
||||||
protected initialize() { |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Parse the value from the model for setting |
|
||||||
*/ |
|
||||||
protected parseValue(val:any) { |
|
||||||
return val; |
|
||||||
} |
|
||||||
} |
|
@ -1,22 +1,25 @@ |
|||||||
import {Injector} from "@angular/core"; |
import {Injector} from "@angular/core"; |
||||||
import { |
import { |
||||||
OpEditingPortalFieldToken, |
OpEditingPortalChangesetToken, |
||||||
OpEditingPortalHandlerToken |
OpEditingPortalHandlerToken, |
||||||
|
OpEditingPortalSchemaToken |
||||||
} from "core-app/modules/fields/edit/edit-field.component"; |
} from "core-app/modules/fields/edit/edit-field.component"; |
||||||
import {PortalInjector} from "@angular/cdk/portal"; |
import {PortalInjector} from "@angular/cdk/portal"; |
||||||
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface"; |
import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler"; |
||||||
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; |
import {IFieldSchema} from "core-app/modules/fields/field.base"; |
||||||
|
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset"; |
||||||
|
|
||||||
/** |
/** |
||||||
* Creates an injector for the edit field portal to pass data into. |
* Creates an injector for the edit field portal to pass data into. |
||||||
* |
* |
||||||
* @returns {PortalInjector} |
* @returns {PortalInjector} |
||||||
*/ |
*/ |
||||||
export function createLocalInjector(injector:Injector, fieldHandler:IEditFieldHandler, field:EditField):Injector { |
export function createLocalInjector(injector:Injector, changeset:WorkPackageChangeset, fieldHandler:EditFieldHandler, schema:IFieldSchema):Injector { |
||||||
const injectorTokens = new WeakMap(); |
const injectorTokens = new WeakMap(); |
||||||
|
|
||||||
|
injectorTokens.set(OpEditingPortalChangesetToken, changeset); |
||||||
injectorTokens.set(OpEditingPortalHandlerToken, fieldHandler); |
injectorTokens.set(OpEditingPortalHandlerToken, fieldHandler); |
||||||
injectorTokens.set(OpEditingPortalFieldToken, field); |
injectorTokens.set(OpEditingPortalSchemaToken, schema); |
||||||
|
|
||||||
return new PortalInjector(injector, injectorTokens); |
return new PortalInjector(injector, injectorTokens); |
||||||
} |
} |
||||||
|
@ -1,83 +0,0 @@ |
|||||||
// -- copyright
|
|
||||||
// OpenProject is a project management system.
|
|
||||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License version 3.
|
|
||||||
//
|
|
||||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
||||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
||||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License
|
|
||||||
// as published by the Free Software Foundation; either version 2
|
|
||||||
// of the License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program; if not, write to the Free Software
|
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
//
|
|
||||||
// See doc/COPYRIGHT.rdoc for more details.
|
|
||||||
// ++
|
|
||||||
|
|
||||||
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; |
|
||||||
import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component"; |
|
||||||
|
|
||||||
export class FormattableEditField extends EditField { |
|
||||||
// Values used in template
|
|
||||||
public instance:FormattableEditFieldComponent; |
|
||||||
public isBusy:boolean = false; |
|
||||||
public isPreview:boolean = false; |
|
||||||
public previewHtml:string = ''; |
|
||||||
public text = { |
|
||||||
attachmentLabel: this.I18n.t('js.label_formattable_attachment_hint'), |
|
||||||
save: this.I18n.t('js.inplace.button_save', { attribute: this.schema.name }), |
|
||||||
cancel: this.I18n.t('js.inplace.button_cancel', { attribute: this.schema.name }) |
|
||||||
}; |
|
||||||
|
|
||||||
public get component() { |
|
||||||
return FormattableEditFieldComponent; |
|
||||||
} |
|
||||||
|
|
||||||
public onSubmit() { |
|
||||||
this.instance |
|
||||||
.getCurrentValue() |
|
||||||
.then((value:string) => { |
|
||||||
this.rawValue = value; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
public get rawValue() { |
|
||||||
if (this.value && this.value.raw) { |
|
||||||
return this.value.raw; |
|
||||||
} else { |
|
||||||
return ''; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public set rawValue(val:string) { |
|
||||||
this.value = { raw: val }; |
|
||||||
} |
|
||||||
|
|
||||||
public get isFormattable() { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
public isEmpty():boolean { |
|
||||||
return !(this.value && this.value.raw); |
|
||||||
} |
|
||||||
|
|
||||||
public submitUnlessInPreview(form:any) { |
|
||||||
setTimeout(() => { |
|
||||||
if (!this.isPreview) { |
|
||||||
form.submit(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,31 @@ |
|||||||
|
import {ErrorHandler, Injectable} from "@angular/core"; |
||||||
|
import {ErrorResource} from "core-app/modules/hal/resources/error-resource"; |
||||||
|
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||||
|
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class HalAwareErrorHandler extends ErrorHandler { |
||||||
|
private text = { |
||||||
|
internal_error: this.I18n.t('js.errors.internal') |
||||||
|
}; |
||||||
|
|
||||||
|
constructor(private readonly I18n:I18nService) { |
||||||
|
super(); |
||||||
|
} |
||||||
|
|
||||||
|
public handleError(error:any) { |
||||||
|
let message:string = this.text.internal_error; |
||||||
|
|
||||||
|
if (error instanceof ErrorResource) { |
||||||
|
console.error("Returned error resource %O", error); |
||||||
|
message += ` ${error.errorMessages.join("\n")}`; |
||||||
|
} else if (error instanceof HalResource) { |
||||||
|
console.error("Returned hal resource %O", error); |
||||||
|
message += `Resource returned ${error.name}`; |
||||||
|
} else { |
||||||
|
message = error; |
||||||
|
} |
||||||
|
|
||||||
|
super.handleError(message); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue