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
Oliver Günther 6 years ago committed by GitHub
parent 650608e798
commit deb448f1fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      frontend/src/app/angular4-modules.ts
  2. 106
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment-field-handler.ts
  3. 5
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html
  4. 78
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts
  5. 49
      frontend/src/app/components/work-packages/work-package-comment/wp-comment-field.component.ts
  6. 7
      frontend/src/app/components/wp-activity/user/user-activity.component.html
  7. 58
      frontend/src/app/components/wp-activity/user/user-activity.component.ts
  8. 8
      frontend/src/app/components/wp-edit-form/single-view-edit-context.ts
  9. 7
      frontend/src/app/components/wp-edit-form/table-row-edit-context.ts
  10. 5
      frontend/src/app/components/wp-edit-form/work-package-edit-context.ts
  11. 38
      frontend/src/app/components/wp-edit-form/work-package-edit-field-handler.ts
  12. 49
      frontend/src/app/components/wp-edit-form/work-package-edit-form.ts
  13. 1
      frontend/src/app/components/wp-edit-form/work-package-editing.service.interface.ts
  14. 8
      frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts
  15. 2
      frontend/src/app/modules/common/ckeditor/op-ckeditor.component.ts
  16. 2
      frontend/src/app/modules/common/notifications/notifications.service.spec.ts
  17. 4
      frontend/src/app/modules/fields/display/display-field.module.ts
  18. 4
      frontend/src/app/modules/fields/display/display-field.service.ts
  19. 82
      frontend/src/app/modules/fields/edit/edit-field.component.ts
  20. 42
      frontend/src/app/modules/fields/edit/edit-field.initializer.ts
  21. 32
      frontend/src/app/modules/fields/edit/edit-field.service.ts
  22. 86
      frontend/src/app/modules/fields/edit/edit.field.module.ts
  23. 47
      frontend/src/app/modules/fields/edit/editing-portal/edit-field-handler.ts
  24. 4
      frontend/src/app/modules/fields/edit/editing-portal/edit-form-portal.component.html
  25. 42
      frontend/src/app/modules/fields/edit/editing-portal/edit-form-portal.component.ts
  26. 15
      frontend/src/app/modules/fields/edit/editing-portal/edit-form-portal.injector.ts
  27. 15
      frontend/src/app/modules/fields/edit/editing-portal/wp-editing-portal-service.ts
  28. 2
      frontend/src/app/modules/fields/edit/field-controls/edit-field-controls.component.html
  29. 8
      frontend/src/app/modules/fields/edit/field-controls/edit-field-controls.component.ts
  30. 17
      frontend/src/app/modules/fields/edit/field-types/boolean-edit-field.component.ts
  31. 20
      frontend/src/app/modules/fields/edit/field-types/date-edit-field.component.ts
  32. 20
      frontend/src/app/modules/fields/edit/field-types/duration-edit-field.component.ts
  33. 14
      frontend/src/app/modules/fields/edit/field-types/float-edit-field.component.ts
  34. 100
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts
  35. 83
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.ts
  36. 15
      frontend/src/app/modules/fields/edit/field-types/integer-edit-field.component.ts
  37. 14
      frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html
  38. 7
      frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts
  39. 6
      frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.html
  40. 10
      frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts
  41. 14
      frontend/src/app/modules/fields/edit/field-types/text-edit-field.component.ts
  42. 18
      frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts
  43. 20
      frontend/src/app/modules/fields/field.base.ts
  44. 7
      frontend/src/app/modules/fields/field.service.ts
  45. 19
      frontend/src/app/modules/fields/openproject-fields.module.ts
  46. 4
      frontend/src/app/modules/hal/openproject-hal.module.ts
  47. 31
      frontend/src/app/modules/hal/services/hal-aware-error-handler.ts
  48. 6
      spec/features/work_packages/attachments/attachment_upload_spec.rb
  49. 2
      spec/features/work_packages/edit_work_package_spec.rb
  50. 4
      spec/features/work_packages/new/new_work_package_spec.rb

@ -219,6 +219,7 @@ import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table
import {ChartsModule} from "ng2-charts"; import {ChartsModule} from "ng2-charts";
import {WorkPackageEmbeddedGraphComponent} from "core-components/wp-table/embedded/wp-embedded-graph.component"; 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 {WorkPackageByVersionGraphComponent} from "core-components/wp-by-version-graph/wp-by-version-graph.component";
import {WorkPackageCommentFieldComponent} from "core-components/work-packages/work-package-comment/wp-comment-field.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -390,6 +391,7 @@ import {WorkPackageByVersionGraphComponent} from "core-components/wp-by-version-
// Activity Tab // Activity Tab
NewestActivityOnOverviewComponent, NewestActivityOnOverviewComponent,
WorkPackageCommentComponent, WorkPackageCommentComponent,
WorkPackageCommentFieldComponent,
ActivityEntryComponent, ActivityEntryComponent,
UserActivityComponent, UserActivityComponent,
RevisionActivityComponent, RevisionActivityComponent,
@ -518,6 +520,7 @@ import {WorkPackageByVersionGraphComponent} from "core-components/wp-by-version-
// Single view // Single view
WorkPackageOverviewTabComponent, WorkPackageOverviewTabComponent,
WorkPackageEditFieldGroupComponent, WorkPackageEditFieldGroupComponent,
WorkPackageCommentFieldComponent,
// Searchbar // Searchbar
ExpandableSearchComponent, ExpandableSearchComponent,

@ -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;
}
}

@ -7,8 +7,9 @@
#commentContainer #commentContainer
*ngIf="canAddComment"> *ngIf="canAddComment">
<div class="wp-edit-field inplace-edit"> <div class="wp-edit-field inplace-edit">
<edit-form-portal *ngIf="active && field" <edit-form-portal *ngIf="active"
[editFieldInput]="field" [schemaInput]="schema"
[changesetInput]="changeset"
[editFieldHandler]="handler"> [editFieldHandler]="handler">
</edit-form-portal> </edit-form-portal>
<div *ngIf="!active" <div *ngIf="!active"

@ -27,7 +27,6 @@
// ++ // ++
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCommentField} from './wp-comment-field.module';
import {ErrorResource} from 'core-app/modules/hal/resources/error-resource'; import {ErrorResource} from 'core-app/modules/hal/resources/error-resource';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
import {WorkPackageCacheService} from '../work-package-cache.service'; import {WorkPackageCacheService} from '../work-package-cache.service';
@ -38,7 +37,7 @@ import {
Component, Component,
ContentChild, ContentChild,
ElementRef, ElementRef,
Inject, Inject, Injector,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
@ -49,22 +48,20 @@ import {ConfigurationService} from "core-app/modules/common/config/configuration
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
import {WorkPackageCommentFieldHandler} from "core-components/work-packages/work-package-comment/work-package-comment-field-handler";
@Component({ @Component({
selector: 'work-package-comment', selector: 'work-package-comment',
templateUrl: './work-package-comment.component.html' templateUrl: './work-package-comment.component.html'
}) })
export class WorkPackageCommentComponent implements IEditFieldHandler, OnInit, OnDestroy { export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler implements OnInit, OnDestroy {
@Input() public workPackage:WorkPackageResource; @Input() public workPackage:WorkPackageResource;
@ContentChild(TemplateRef) template:TemplateRef<any>; @ContentChild(TemplateRef) template:TemplateRef<any>;
@ViewChild('commentContainer') public commentContainer:ElementRef; @ViewChild('commentContainer') public commentContainer:ElementRef;
public field:WorkPackageCommentField;
public handler:IEditFieldHandler = this;
public text = { public text = {
editTitle: this.I18n.t('js.label_add_comment_title'), editTitle: this.I18n.t('js.label_add_comment_title'),
addComment: this.I18n.t('js.label_add_comment'), addComment: this.I18n.t('js.label_add_comment'),
@ -73,11 +70,13 @@ export class WorkPackageCommentComponent implements IEditFieldHandler, OnInit, O
}; };
public fieldLabel:string = this.text.editTitle; public fieldLabel:string = this.text.editTitle;
public editing = false; public inFlight = false;
public canAddComment:boolean; public canAddComment:boolean;
public showAbove:boolean; public showAbove:boolean;
public changeset:WorkPackageChangeset;
constructor(protected elementRef:ElementRef, constructor(protected elementRef:ElementRef,
protected injector:Injector,
protected commentService:CommentService, protected commentService:CommentService,
protected wpLinkedActivities:WorkPackagesActivityService, protected wpLinkedActivities:WorkPackagesActivityService,
protected ConfigurationService:ConfigurationService, protected ConfigurationService:ConfigurationService,
@ -86,10 +85,12 @@ export class WorkPackageCommentComponent implements IEditFieldHandler, OnInit, O
protected wpNotificationsService:WorkPackageNotificationService, protected wpNotificationsService:WorkPackageNotificationService,
protected NotificationsService:NotificationsService, protected NotificationsService:NotificationsService,
protected I18n:I18nService) { protected I18n:I18nService) {
super(elementRef, injector);
} }
public ngOnInit() { public ngOnInit() {
super.ngOnInit();
this.canAddComment = !!this.workPackage.addComment; this.canAddComment = !!this.workPackage.addComment;
this.showAbove = this.ConfigurationService.commentsSortedInDescendingOrder(); this.showAbove = this.ConfigurationService.commentsSortedInDescendingOrder();
@ -124,57 +125,39 @@ export class WorkPackageCommentComponent implements IEditFieldHandler, OnInit, O
return 'wp-comment-field'; return 'wp-comment-field';
} }
public get active() {
return this.editing;
}
public get inEditMode() {
return false;
}
public activate(withText?:string) { public activate(withText?:string) {
this.editing = true; super.activate(withText);
this.reset(withText);
if (!this.showAbove) { if (!this.showAbove) {
this.scrollToBottom(); this.scrollToBottom();
} }
} }
public get project() {
return this.workPackage.project;
}
public reset(withText?:string) {
this.field = this.field || new WorkPackageCommentField(this.workPackage);
this.field.initializeFieldValue(withText);
}
public deactivate(focus:boolean) { public deactivate(focus:boolean) {
focus && this.focus(); focus && this.focus();
this.editing = false; this.inEdit = false;
} }
public handleUserSubmit() { public async handleUserSubmit() {
if (this.field.isBusy || this.field.isEmpty()) { if (this.inFlight || !this.rawComment) {
return Promise.resolve(); return Promise.resolve();
} }
this.field.isBusy = true; this.inFlight = true;
await this.onSubmit();
let indicator = this.loadingIndicator.wpDetails; let indicator = this.loadingIndicator.wpDetails;
return indicator.promise = this.commentService.createComment(this.workPackage, this.field.value) return indicator.promise = this.commentService.createComment(this.workPackage, this.commentValue)
.then(() => { .then(() => {
this.editing = false; this.inEdit = false;
this.NotificationsService.addSuccess(this.I18n.t('js.work_packages.comment_added')); this.NotificationsService.addSuccess(this.I18n.t('js.work_packages.comment_added'));
this.wpLinkedActivities.require(this.workPackage, true); this.wpLinkedActivities.require(this.workPackage, true);
this.wpCacheService.updateWorkPackage(this.workPackage); this.wpCacheService.updateWorkPackage(this.workPackage);
this.field.isBusy = false; this.inFlight = false;
this.focus(); this.focus();
}) })
.catch((error:any) => { .catch((error:any) => {
this.field.isBusy = false; this.inFlight = false;
if (error instanceof ErrorResource) { if (error instanceof ErrorResource) {
this.wpNotificationsService.showError(error, this.workPackage); this.wpNotificationsService.showError(error, this.workPackage);
} }
@ -184,27 +167,6 @@ export class WorkPackageCommentComponent implements IEditFieldHandler, OnInit, O
}); });
} }
public handleUserCancel() {
this.deactivate(true);
}
focus():void {
const trigger = this.elementRef.nativeElement.querySelector('.inplace-editing--trigger-container');
trigger && trigger.focus();
}
handleUserKeydown(event:JQueryEventObject, onlyCancel?:boolean):void {
// We only save comments through field controls
}
isChanged():boolean {
return false;
}
stopPropagation(evt:JQueryEventObject):boolean {
return false;
}
scrollToBottom():void { scrollToBottom():void {
const scrollableContainer = jQuery(this.elementRef.nativeElement).scrollParent()[0]; const scrollableContainer = jQuery(this.elementRef.nativeElement).scrollParent()[0];
if (scrollableContainer) { if (scrollableContainer) {

@ -26,50 +26,31 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChangeset} from '../../wp-edit-form/work-package-changeset';
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {FormattableEditField} from "core-app/modules/fields/edit/field-types/formattable-edit-field"; import {Component, OnInit} from "@angular/core";
import {
export class WorkPackageCommentField extends FormattableEditField { FormattableEditFieldComponent,
public _value:any; formattableFieldTemplate
} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
@Component({
template: formattableFieldTemplate
})
export class WorkPackageCommentFieldComponent extends FormattableEditFieldComponent implements OnInit {
public isBusy:boolean = false; public isBusy:boolean = false;
public ConfigurationService:ConfigurationService = this.$injector.get(ConfigurationService); public ConfigurationService:ConfigurationService = this.$injector.get(ConfigurationService);
constructor(public workPackage:WorkPackageResource) { public get name() {
super( return 'comment';
new WorkPackageChangeset(WorkPackageCommentField.$injector, workPackage),
'comment',
{name: I18n.t('js.label_comment')} as any
);
this.initializeFieldValue();
}
public get value() {
return this._value;
}
public set value(val:any) {
this._value = val;
} }
public get required() { public get required() {
return true; return true;
} }
public initializeFieldValue(withText?:string):void { ngOnInit() {
if (!withText) { super.ngOnInit();
this.rawValue = ''; this.rawValue = this.rawValue || '';
return;
}
if (this.rawValue.length > 0) {
this.rawValue += '\n';
}
this.rawValue += withText;
} }
} }

@ -34,17 +34,18 @@
<op-icon icon-classes="action-icon icon-quote" [icon-title]="text.quote_comment"></op-icon> <op-icon icon-classes="action-icon icon-quote" [icon-title]="text.quote_comment"></op-icon>
</accessible-by-keyboard> </accessible-by-keyboard>
<accessible-by-keyboard *ngIf="userCanEdit" <accessible-by-keyboard *ngIf="userCanEdit"
(execute)="editComment()" (execute)="activate()"
[linkTitle]="text.edit_comment" [linkTitle]="text.edit_comment"
class="edit-activity--{{activityNo}}"> class="edit-activity--{{activityNo}}">
<op-icon icon-classes="action-icon icon-edit" [icon-title]="text.edit_comment"></op-icon> <op-icon icon-classes="action-icon icon-edit" [icon-title]="text.edit_comment"></op-icon>
</accessible-by-keyboard> </accessible-by-keyboard>
</div> </div>
</div> </div>
<div class="user-comment wiki"> <div class="user-comment" [ngClass]="{ 'wiki': !inEdit }">
<div *ngIf="inEdit" class="inplace-edit"> <div *ngIf="inEdit" class="inplace-edit">
<div class="user-comment--form inplace-edit--write-value"> <div class="user-comment--form inplace-edit--write-value">
<edit-form-portal [editFieldInput]="field" <edit-form-portal [changesetInput]="changeset"
[schemaInput]="schema"
[editFieldHandler]="handler"> [editFieldHandler]="handler">
</edit-form-portal> </edit-form-portal>
</div> </div>

@ -30,29 +30,25 @@ import {UserResource} from 'core-app/modules/hal/resources/user-resource';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {WorkPackageCommentField} from 'core-components/work-packages/work-package-comment/wp-comment-field.module';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service'; import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service';
import {AfterViewInit, Component, ElementRef, Inject, Input, OnInit} from "@angular/core"; import {AfterViewInit, Component, ElementRef, Injector, Input, OnInit} from "@angular/core";
import {UserCacheService} from "core-components/user/user-cache.service"; import {UserCacheService} from "core-components/user/user-cache.service";
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface";
import {CommentService} from "core-components/wp-activity/comment-service"; import {CommentService} from "core-components/wp-activity/comment-service";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageCommentFieldHandler} from "core-components/work-packages/work-package-comment/work-package-comment-field-handler";
@Component({ @Component({
selector: 'user-activity', selector: 'user-activity',
templateUrl: './user-activity.component.html' templateUrl: './user-activity.component.html'
}) })
export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterViewInit { export class UserActivityComponent extends WorkPackageCommentFieldHandler implements OnInit, AfterViewInit {
@Input() public workPackage:WorkPackageResource; @Input() public workPackage:WorkPackageResource;
@Input() public activity:any; @Input() public activity:any;
@Input() public activityNo:number; @Input() public activityNo:number;
@Input() public activityLabel:string; @Input() public activityLabel:string;
@Input() public isInitial:boolean; @Input() public isInitial:boolean;
public handler = this;
public inEdit = false;
public inEditMode = false;
public userCanEdit = false; public userCanEdit = false;
public userCanQuote = false; public userCanQuote = false;
@ -66,7 +62,6 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
public details:any[] = []; public details:any[] = [];
public isComment:boolean; public isComment:boolean;
public field:WorkPackageCommentField;
public focused = false; public focused = false;
public text = { public text = {
@ -80,6 +75,7 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
private $element:JQuery; private $element:JQuery;
constructor(readonly elementRef:ElementRef, constructor(readonly elementRef:ElementRef,
readonly injector:Injector,
readonly PathHelper:PathHelperService, readonly PathHelper:PathHelperService,
readonly wpLinkedActivities:WorkPackagesActivityService, readonly wpLinkedActivities:WorkPackagesActivityService,
readonly commentService:CommentService, readonly commentService:CommentService,
@ -87,9 +83,12 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
readonly ConfigurationService:ConfigurationService, readonly ConfigurationService:ConfigurationService,
readonly userCacheService:UserCacheService, readonly userCacheService:UserCacheService,
readonly I18n:I18nService) { readonly I18n:I18nService) {
super(elementRef, injector);
} }
public ngOnInit() { public ngOnInit() {
super.ngOnInit();
this.isComment = this.activity._type === 'Activity::Comment'; this.isComment = this.activity._type === 'Activity::Comment';
this.$element = jQuery(this.elementRef.nativeElement); this.$element = jQuery(this.elementRef.nativeElement);
this.reset(); this.reset();
@ -137,37 +136,24 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
} }
} }
public reset(withText?:string) { public activate() {
this.field = new WorkPackageCommentField(this.workPackage); super.activate(this.activity.comment.raw);
this.field.initializeFieldValue(withText);
} }
public handleUserSubmit() { public handleUserSubmit() {
if (this.field.isBusy || this.field.isEmpty()) { if (this.changeset.inFlight || !this.rawComment) {
return Promise.resolve(); return Promise.resolve();
} }
return this.updateComment(); return this.updateComment();
} }
public handleUserCancel() {
this.deactivate(true);
}
public get active() {
return this.inEdit;
}
public editComment() {
this.inEdit = true;
this.reset(this.activity.comment.raw);
}
public quoteComment() { public quoteComment() {
this.commentService.quoteEvents.next(this.quotedText(this.activity.comment.raw)); this.commentService.quoteEvents.next(this.quotedText(this.activity.comment.raw));
} }
public updateComment() { public async updateComment() {
return this.commentService.updateComment(this.activity, this.field.rawValue || '') await this.onSubmit();
return this.commentService.updateComment(this.activity, this.rawComment || '')
.then(() => { .then(() => {
this.wpLinkedActivities.require(this.workPackage, true); this.wpLinkedActivities.require(this.workPackage, true);
this.wpCacheService.updateWorkPackage(this.workPackage); this.wpCacheService.updateWorkPackage(this.workPackage);
@ -205,27 +191,11 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
return `user_activity_edit_field_${this.activityNo}`; return `user_activity_edit_field_${this.activityNo}`;
} }
public get project() {
return this.workPackage.project;
}
deactivate(focus:boolean):void { deactivate(focus:boolean):void {
this.inEdit = false; super.deactivate(focus);
if (focus) { if (focus) {
this.focusEditIcon(); this.focusEditIcon();
} }
} }
handleUserKeydown(event:JQueryEventObject, onlyCancel?:boolean):void {
}
isChanged():boolean {
return false;
}
stopPropagation(evt:JQueryEventObject):boolean {
return false;
}
} }

@ -39,8 +39,8 @@ import {WorkPackageTableRefreshService} from 'core-components/wp-table/wp-table-
import {WorkPackageEditForm} from 'core-components/wp-edit-form/work-package-edit-form'; import {WorkPackageEditForm} from 'core-components/wp-edit-form/work-package-edit-form';
import {WorkPackageEditFieldHandler} from 'core-components/wp-edit-form/work-package-edit-field-handler'; import {WorkPackageEditFieldHandler} from 'core-components/wp-edit-form/work-package-edit-field-handler';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper'; import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service";
import {IFieldSchema} from "core-app/modules/fields/field.base";
export class SingleViewEditContext implements WorkPackageEditContext { export class SingleViewEditContext implements WorkPackageEditContext {
@ -60,14 +60,14 @@ export class SingleViewEditContext implements WorkPackageEditContext {
readonly fieldGroup:WorkPackageEditFieldGroupComponent) { readonly fieldGroup:WorkPackageEditFieldGroupComponent) {
} }
public async activateField(form:WorkPackageEditForm, field:EditField, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler> { public async activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler> {
return this.fieldCtrl(field.name).then((ctrl) => { return this.fieldCtrl(fieldName).then((ctrl) => {
ctrl.active = true; ctrl.active = true;
const container = ctrl.editContainer.nativeElement; const container = ctrl.editContainer.nativeElement;
return this.wpEditingPortalService.create( return this.wpEditingPortalService.create(
container, container,
form, form,
field, schema,
fieldName, fieldName,
errors errors
); );

@ -37,9 +37,8 @@ import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler';
import {WorkPackageEditForm} from './work-package-edit-form'; import {WorkPackageEditForm} from './work-package-edit-form';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper'; import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table'; import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table';
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service";
import {asyncTimeOutput} from "core-app/helpers/debug_output"; import {IFieldSchema} from "core-app/modules/fields/field.base";
export class TableRowEditContext implements WorkPackageEditContext { export class TableRowEditContext implements WorkPackageEditContext {
@ -71,7 +70,7 @@ export class TableRowEditContext implements WorkPackageEditContext {
return this.rowContainer.find(`.${tdClassName}.${fieldName}`).first(); return this.rowContainer.find(`.${tdClassName}.${fieldName}`).first();
} }
public activateField(form:WorkPackageEditForm, field:EditField, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler> { public activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler> {
return this.waitForContainer(fieldName) return this.waitForContainer(fieldName)
.then((cell) => { .then((cell) => {
@ -85,7 +84,7 @@ export class TableRowEditContext implements WorkPackageEditContext {
return this.wpEditingPortalService.create( return this.wpEditingPortalService.create(
cell, cell,
form, form,
field, schema,
fieldName, fieldName,
errors errors
); );

@ -29,12 +29,13 @@
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageEditForm} from './work-package-edit-form'; import {WorkPackageEditForm} from './work-package-edit-form';
import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler'; import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler';
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; import {IFieldSchema} from "core-app/modules/fields/field.base";
export interface WorkPackageEditContext { export interface WorkPackageEditContext {
/** /**
* Activate the field, returning the element and associated field handler * Activate the field, returning the element and associated field handler
*/ */
activateField(form:WorkPackageEditForm, field:EditField, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler>; activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise<WorkPackageEditFieldHandler>;
/** /**
* Show this required field. E.g., add the necessary column * Show this required field. E.g., add the necessary column

@ -32,15 +32,16 @@ import {WorkPackageEditContext} from './work-package-edit-context';
import {keyCodes} from 'core-app/modules/common/keyCodes.enum'; import {keyCodes} from 'core-app/modules/common/keyCodes.enum';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {ComponentRef, Injector} from '@angular/core'; import {Injector} from '@angular/core';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper'; import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler";
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface";
import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position"; import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position";
import {debugLog} from "core-app/helpers/debug_output"; import {debugLog} from "core-app/helpers/debug_output";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {Subject} from 'rxjs';
export class WorkPackageEditFieldHandler implements IEditFieldHandler { export class WorkPackageEditFieldHandler extends EditFieldHandler {
// Injections // Injections
readonly FocusHelper:FocusHelperService = this.injector.get(FocusHelperService) readonly FocusHelper:FocusHelperService = this.injector.get(FocusHelperService)
readonly ConfigurationService = this.injector.get(ConfigurationService); readonly ConfigurationService = this.injector.get(ConfigurationService);
@ -48,10 +49,9 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
// Other fields // Other fields
public editContext:WorkPackageEditContext; public editContext:WorkPackageEditContext;
public schemaName:string;
// Reference to the active component, if any // Reference to the active component, if any
public componentInstance:ComponentRef<EditFieldComponent>; public componentInstance:EditFieldComponent;
// Current errors of the field // Current errors of the field
public errors:string[]; public errors:string[];
@ -59,13 +59,11 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
constructor(public injector:Injector, constructor(public injector:Injector,
public form:WorkPackageEditForm, public form:WorkPackageEditForm,
public fieldName:string, public fieldName:string,
public field:EditField, public schema:IFieldSchema,
public element:HTMLElement, public element:HTMLElement,
protected onDestroy:() => void,
protected withErrors?:string[]) { protected withErrors?:string[]) {
super();
this.editContext = form.editContext; this.editContext = form.editContext;
this.schemaName = field.name;
if (withErrors !== undefined) { if (withErrors !== undefined) {
this.setErrors(withErrors); this.setErrors(withErrors);
@ -84,6 +82,10 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
return this.form.editMode; return this.form.editMode;
} }
public get inFlight() {
return this.form.changeset.inFlight;
}
public get active() { public get active() {
return true; return true;
} }
@ -110,16 +112,19 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
this.element.classList.toggle('-error', this.isErrorenous); this.element.classList.toggle('-error', this.isErrorenous);
} }
/** /**
* Handle a user submitting the field (e.g, ng-change) * Handle a user submitting the field (e.g, ng-change)
*/ */
public handleUserSubmit():Promise<any> { public handleUserSubmit():Promise<any> {
if (this.field.inFlight || this.form.editMode) { if (this.form.changeset.inFlight || this.form.editMode) {
return Promise.resolve(); return Promise.resolve();
} }
this.field.onSubmit(); return this
return this.form.submit(); .onSubmit()
.then(() => this.form.submit());
} }
/** /**
@ -169,7 +174,8 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
*/ */
public deactivate(focus:boolean = false) { public deactivate(focus:boolean = false) {
delete this.form.activeFields[this.fieldName]; delete this.form.activeFields[this.fieldName];
this.onDestroy(); this.onDestroy.next();
this.onDestroy.complete();
this.editContext.reset(this.workPackage, this.fieldName, focus); this.editContext.reset(this.workPackage, this.fieldName, focus);
} }
@ -184,7 +190,7 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
* Returns whether the field has been changed * Returns whether the field has been changed
*/ */
public isChanged():boolean { public isChanged():boolean {
return this.form.changeset.isOverridden(this.schemaName); return this.form.changeset.isOverridden(this.fieldName);
} }
/** /**
@ -213,7 +219,7 @@ export class WorkPackageEditFieldHandler implements IEditFieldHandler {
* Return the field label * Return the field label
*/ */
public get fieldLabel() { public get fieldLabel() {
return this.field.displayName; return this.schema.name || this.fieldName;
} }
public get errorMessageOnLabel() { public get errorMessageOnLabel() {

@ -31,7 +31,6 @@ import {ErrorResource} from 'core-app/modules/hal/resources/error-resource';
import {FormResource} from 'core-app/modules/hal/resources/form-resource'; import {FormResource} from 'core-app/modules/hal/resources/form-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {debugLog} from '../../helpers/debug_output';
import {States} from '../states.service'; import {States} from '../states.service';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service'; import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
@ -39,10 +38,8 @@ import {WorkPackageTableRefreshService} from '../wp-table/wp-table-refresh-reque
import {WorkPackageChangeset} from './work-package-changeset'; import {WorkPackageChangeset} from './work-package-changeset';
import {WorkPackageEditContext} from './work-package-edit-context'; import {WorkPackageEditContext} from './work-package-edit-context';
import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler'; import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler';
import {WorkPackageEditingService} from './work-package-editing-service';
import {EditFieldService} from "core-app/modules/fields/edit/edit-field.service";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface"; import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {IFieldSchema} from "core-app/modules/fields/field.base";
export const activeFieldContainerClassName = 'wp-inline-edit--active-field'; export const activeFieldContainerClassName = 'wp-inline-edit--active-field';
export const activeFieldClassName = 'wp-inline-edit--field'; export const activeFieldClassName = 'wp-inline-edit--field';
@ -52,7 +49,6 @@ export class WorkPackageEditForm {
public states:States = this.injector.get(States); public states:States = this.injector.get(States);
public wpCacheService = this.injector.get(WorkPackageCacheService); public wpCacheService = this.injector.get(WorkPackageCacheService);
public wpEditing = this.injector.get(IWorkPackageEditingServiceToken); public wpEditing = this.injector.get(IWorkPackageEditingServiceToken);
public wpEditField = this.injector.get(EditFieldService);
public wpTableRefresh = this.injector.get(WorkPackageTableRefreshService); public wpTableRefresh = this.injector.get(WorkPackageTableRefreshService);
public wpNotificationsService = this.injector.get(WorkPackageNotificationService); public wpNotificationsService = this.injector.get(WorkPackageNotificationService);
@ -114,14 +110,14 @@ export class WorkPackageEditForm {
*/ */
public activate(fieldName:string, noWarnings:boolean = false):Promise<WorkPackageEditFieldHandler> { public activate(fieldName:string, noWarnings:boolean = false):Promise<WorkPackageEditFieldHandler> {
return new Promise<WorkPackageEditFieldHandler>((resolve, reject) => { return new Promise<WorkPackageEditFieldHandler>((resolve, reject) => {
this.buildField(fieldName) this.loadFieldSchema(fieldName)
.then((field:EditField) => { .then((schema:IFieldSchema) => {
if (!field.writable && !noWarnings) { if (!schema.writable && !noWarnings) {
this.wpNotificationsService.showEditingBlockedError(field.displayName); this.wpNotificationsService.showEditingBlockedError(schema.name || fieldName);
reject(); reject();
} }
this.renderField(fieldName, field) this.renderField(fieldName, schema)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
}); });
@ -161,7 +157,7 @@ export class WorkPackageEditForm {
* Save the active changeset. * Save the active changeset.
* @return {any} * @return {any}
*/ */
public submit():Promise<WorkPackageResource> { public async submit():Promise<WorkPackageResource> {
const isInitial = this.workPackage.isNew; const isInitial = this.workPackage.isNew;
if (this.changeset.empty && !isInitial) { if (this.changeset.empty && !isInitial) {
@ -174,7 +170,9 @@ export class WorkPackageEditForm {
// Notify all fields of upcoming save // Notify all fields of upcoming save
const openFields = _.keys(this.activeFields); const openFields = _.keys(this.activeFields);
_.each(this.activeFields, (handler:WorkPackageEditFieldHandler) => handler.field.onSubmit());
// Call onSubmit handlers
await Promise.all(_.map(this.activeFields, (handler:WorkPackageEditFieldHandler) => handler.onSubmit()));
return new Promise<WorkPackageResource>((resolve, reject) => { return new Promise<WorkPackageResource>((resolve, reject) => {
this.changeset.save() this.changeset.save()
@ -193,7 +191,8 @@ export class WorkPackageEditForm {
this.wpNotificationsService.handleRawError(error, this.workPackage); this.wpNotificationsService.handleRawError(error, this.workPackage);
if (error instanceof ErrorResource) { if (error instanceof ErrorResource) {
this.handleSubmissionErrors(error, reject); this.handleSubmissionErrors(error);
reject();
} }
}); });
}); });
@ -230,10 +229,9 @@ export class WorkPackageEditForm {
}); });
} }
protected handleSubmissionErrors(error:any, reject:Function) { protected handleSubmissionErrors(error:any) {
// Process single API errors // Process single API errors
this.handleErroneousAttributes(error); this.handleErroneousAttributes(error);
return reject();
} }
protected handleErroneousAttributes(error:any) { protected handleErroneousAttributes(error:any) {
@ -275,8 +273,13 @@ export class WorkPackageEditForm {
}); });
} }
private buildField(fieldName:string):Promise<EditField> { /**
return new Promise<EditField>((resolve, reject) => { * Load the work package form to get the current field schema with all
* values loaded.
* @param fieldName
*/
private loadFieldSchema(fieldName:string):Promise<IFieldSchema> {
return new Promise<IFieldSchema>((resolve, reject) => {
this.changeset.getForm() this.changeset.getForm()
.then((form:FormResource) => { .then((form:FormResource) => {
const schemaName = this.changeset.getSchemaName(fieldName); const schemaName = this.changeset.getSchemaName(fieldName);
@ -286,13 +289,7 @@ export class WorkPackageEditForm {
return reject(); return reject();
} }
const field = this.wpEditField.getField( resolve(fieldSchema);
this.changeset,
schemaName,
fieldSchema
);
resolve(field);
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to build edit field: %o', error); console.error('Failed to build edit field: %o', error);
@ -301,9 +298,9 @@ export class WorkPackageEditForm {
}); });
} }
private renderField(fieldName:string, field:EditField):Promise<WorkPackageEditFieldHandler> { private renderField(fieldName:string, schema:IFieldSchema):Promise<WorkPackageEditFieldHandler> {
const promise:Promise<WorkPackageEditFieldHandler> = this.editContext.activateField(this, const promise:Promise<WorkPackageEditFieldHandler> = this.editContext.activateField(this,
field, schema,
fieldName, fieldName,
this.errorsPerAttribute[fieldName] || []); this.errorsPerAttribute[fieldName] || []);

@ -27,7 +27,6 @@
// ++ // ++
import {InjectionToken} from "@angular/core"; import {InjectionToken} from "@angular/core";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
export const IWorkPackageEditingServiceToken = new InjectionToken<any>('IWorkPackageEditingService'); export const IWorkPackageEditingServiceToken = new InjectionToken<any>('IWorkPackageEditingService');

@ -34,10 +34,10 @@ export class WorkPackageRelationRowComponent implements OnInit {
// Create a quasi-field object // Create a quasi-field object
public fieldController = { public fieldController = {
active: true, handler: {
field: { active: true,
required: false },
} required: false
}; };
public relation:RelationResource; public relation:RelationResource;

@ -72,7 +72,7 @@ export class OpCkeditorComponent implements OnInit, OnDestroy {
// Debounce change listener for both CKE and codemirror // Debounce change listener for both CKE and codemirror
// to read back changes as they happen // to read back changes as they happen
private debouncedEmitter = _.debounce( private debouncedEmitter = _.debounce(
async () => { () => {
this.getTransformedContent(false) this.getTransformedContent(false)
.then(val => { .then(val => {
this.onContentChange.emit(val); this.onContentChange.emit(val);

@ -30,6 +30,7 @@ import {OpenprojectHalModule} from 'core-app/modules/hal/openproject-hal.module'
import {async, TestBed} from '@angular/core/testing'; import {async, TestBed} from '@angular/core/testing';
import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service';
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
describe('NotificationsService', function () { describe('NotificationsService', function () {
var notificationsService:NotificationsService; var notificationsService:NotificationsService;
@ -42,6 +43,7 @@ describe('NotificationsService', function () {
], ],
providers: [ providers: [
{ provide: ConfigurationService, useValue: { autoHidePopups: () => true } }, { provide: ConfigurationService, useValue: { autoHidePopups: () => true } },
I18nService,
NotificationsService, NotificationsService,
] ]
}) })

@ -36,11 +36,13 @@ export class DisplayField extends Field {
public mode:string | null = null; public mode:string | null = null;
public changeset:WorkPackageChangeset|null = null; public changeset:WorkPackageChangeset|null = null;
protected I18n:I18nService
constructor(public resource:any, constructor(public resource:any,
public name:string, public name:string,
public schema:IFieldSchema, public schema:IFieldSchema,
public context:DisplayFieldContext) { public context:DisplayFieldContext) {
super(resource, name, schema); super();
this.I18n = this.$injector.get(I18nService);
} }
public get isFormattable():boolean { public get isFormattable():boolean {

@ -66,9 +66,7 @@ export class DisplayFieldService extends AbstractFieldService<DisplayField, IDis
* @returns {T} * @returns {T}
*/ */
public getField(resource:any, fieldName:string, schema:IFieldSchema, context:DisplayFieldContext):DisplayField { public getField(resource:any, fieldName:string, schema:IFieldSchema, context:DisplayFieldContext):DisplayField {
let type = this.fieldType(fieldName) || this.fieldType(schema.type) || this.defaultFieldType; const fieldClass = this.getClassFor(fieldName, schema.type);
let fieldClass:IDisplayFieldType = this.classes[type];
return new fieldClass(resource, fieldName, schema, context); return new fieldClass(resource, fieldName, schema, context);
} }
} }

@ -26,46 +26,63 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {ChangeDetectorRef, Component, Inject, InjectionToken, Injector, OnDestroy, OnInit} from "@angular/core"; import {
import {WorkPackageEditFieldHandler} from "core-components/wp-edit-form/work-package-edit-field-handler"; ChangeDetectorRef,
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; Component,
import {IFieldSchema} from "core-app/modules/fields/field.base"; ElementRef,
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface"; Inject,
InjectionToken,
Injector,
OnDestroy,
OnInit
} from "@angular/core";
import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface"; import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {Field, IFieldSchema} from "core-app/modules/fields/field.base";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
export const OpEditingPortalFieldToken = new InjectionToken('wp-editing-portal--field'); export const OpEditingPortalSchemaToken = new InjectionToken('wp-editing-portal--schema');
export const OpEditingPortalHandlerToken = new InjectionToken('wp-editing-portal--handler'); export const OpEditingPortalHandlerToken = new InjectionToken('wp-editing-portal--handler');
export const OpEditingPortalChangesetToken = new InjectionToken('wp-editing-portal--changeset');
@Component({ @Component({
template: '' template: ''
}) })
export class EditFieldComponent implements OnDestroy { export class EditFieldComponent extends Field implements OnDestroy {
/** Self reference */
public self = this;
constructor(readonly I18n:I18nService, constructor(readonly I18n:I18nService,
readonly elementRef:ElementRef,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService, @Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService,
@Inject(OpEditingPortalFieldToken) readonly field:EditField, @Inject(OpEditingPortalChangesetToken) protected changeset:WorkPackageChangeset,
@Inject(OpEditingPortalHandlerToken) readonly handler:IEditFieldHandler, @Inject(OpEditingPortalSchemaToken) public schema:IFieldSchema,
@Inject(OpEditingPortalHandlerToken) readonly handler:EditFieldHandler,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
readonly injector:Injector) { readonly injector:Injector) {
super();
this.initialize(); this.initialize();
this.wpEditing.state(this.field.resource.id) this.wpEditing.state(this.changeset.workPackage.id)
.values$() .values$()
.pipe( .pipe(
untilComponentDestroyed(this) untilComponentDestroyed(this)
) )
.subscribe((changeset) => { .subscribe((changeset) => {
if (!this.changeset.empty && this.changeset.wpForm.hasValue()) { if (!this.changeset.empty && this.changeset.wpForm.hasValue()) {
const fieldSchema = changeset.wpForm.value!.schema[this.field.name]; const fieldSchema = changeset.wpForm.value!.schema[this.name];
if (!fieldSchema) { if (!fieldSchema) {
return handler.deactivate(false); return handler.deactivate(false);
} }
this.field.schema = fieldSchema; this.changeset = changeset;
this.field.resource = changeset.workPackage; this.schema = fieldSchema;
this.initialize(); this.initialize();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -76,31 +93,46 @@ export class EditFieldComponent implements OnDestroy {
// Nothing to do // Nothing to do
} }
protected initialize() { public get inFlight() {
// Allow subclasses to create post-constructor initialization return this.handler.inFlight;
} }
public get value() { public get value() {
return this.field.value; return this.changeset.value(this.name);
} }
public set value(val:any) { public get name() {
this.field.value = val; // Get the mapped schema name, as this is not always the attribute
// e.g., startDate in table for milestone => date attribute
return this.changeset.getSchemaName(this.handler.fieldName);
} }
public get name() { public set value(value:any) {
return this.field.name; this.changeset.setValue(this.name, this.parseValue(value));
} }
public get schema():IFieldSchema { public get placeholder() {
return this.field.schema; if (this.name === 'subject') {
return this.I18n.t('js.placeholders.subject');
}
return '';
} }
public get resource() { public get resource() {
return this.field.resource; return this.changeset.workPackage;
}
/**
* Initialize the field after constructor was called.
*/
protected initialize() {
} }
public get changeset() { /**
return this.field.changeset; * Parse the value from the model for setting
*/
protected parseValue(val:any) {
return val;
} }
} }

@ -27,26 +27,27 @@
// ++ // ++
import {EditFieldService} from "core-app/modules/fields/edit/edit-field.service"; import {EditFieldService} from "core-app/modules/fields/edit/edit-field.service";
import {TextEditField} from "core-app/modules/fields/edit/field-types/text-edit-field"; import {TextEditFieldComponent} from "core-app/modules/fields/edit/field-types/text-edit-field.component";
import {IntegerEditField} from "core-app/modules/fields/edit/field-types/integer-edit-field"; import {IntegerEditFieldComponent} from "core-app/modules/fields/edit/field-types/integer-edit-field.component";
import {DurationEditField} from "core-app/modules/fields/edit/field-types/duration-edit-field"; import {DurationEditFieldComponent} from "core-app/modules/fields/edit/field-types/duration-edit-field.component";
import {SelectEditField} from "core-app/modules/fields/edit/field-types/select-edit-field"; import {SelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/select-edit-field.component";
import {MultiSelectEditField} from "core-app/modules/fields/edit/field-types/multi-select-edit-field"; import {MultiSelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/multi-select-edit-field.component";
import {FloatEditField} from "core-app/modules/fields/edit/field-types/float-edit-field"; import {FloatEditFieldComponent} from "core-app/modules/fields/edit/field-types/float-edit-field.component";
import {WorkPackageEditField} from "core-app/modules/fields/edit/field-types/work-package-edit-field.module"; import {BooleanEditFieldComponent} from "core-app/modules/fields/edit/field-types/boolean-edit-field.component";
import {BooleanEditField} from "core-app/modules/fields/edit/field-types/boolean-edit-field"; import {WorkPackageEditFieldComponent} from "core-app/modules/fields/edit/field-types/work-package-edit-field.component";
import {DateEditField} from "core-app/modules/fields/edit/field-types/date-edit-field"; import {DateEditFieldComponent} from "core-app/modules/fields/edit/field-types/date-edit-field.component";
import {FormattableEditField} from "core-app/modules/fields/edit/field-types/formattable-edit-field"; import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
import {WorkPackageCommentFieldComponent} from "core-components/work-packages/work-package-comment/wp-comment-field.component";
export function initializeCoreEditFields(editFieldService:EditFieldService) { export function initializeCoreEditFields(editFieldService:EditFieldService) {
return () => { return () => {
editFieldService.defaultFieldType = 'text'; editFieldService.defaultFieldType = 'text';
editFieldService editFieldService
.addFieldType(TextEditField, 'text', ['String']) .addFieldType(TextEditFieldComponent, 'text', ['String'])
.addFieldType(IntegerEditField, 'integer', ['Integer']) .addFieldType(IntegerEditFieldComponent, 'integer', ['Integer'])
.addFieldType(DurationEditField, 'duration', ['Duration']) .addFieldType(DurationEditFieldComponent, 'duration', ['Duration'])
.addFieldType(SelectEditField, 'select', ['Priority', .addFieldType(SelectEditFieldComponent, 'select', ['Priority',
'Status', 'Status',
'Type', 'Type',
'User', 'User',
@ -54,14 +55,15 @@ export function initializeCoreEditFields(editFieldService:EditFieldService) {
'Category', 'Category',
'CustomOption', 'CustomOption',
'Project']) 'Project'])
.addFieldType(MultiSelectEditField, 'multi-select', [ .addFieldType(MultiSelectEditFieldComponent, 'multi-select', [
'[]CustomOption', '[]CustomOption',
'[]User' '[]User'
]) ])
.addFieldType(FloatEditField, 'float', ['Float']) .addFieldType(FloatEditFieldComponent, 'float', ['Float'])
.addFieldType(WorkPackageEditField, 'workPackage', ['WorkPackage']) .addFieldType(WorkPackageEditFieldComponent, 'workPackage', ['WorkPackage'])
.addFieldType(BooleanEditField, 'boolean', ['Boolean']) .addFieldType(BooleanEditFieldComponent, 'boolean', ['Boolean'])
.addFieldType(DateEditField, 'date', ['Date']) .addFieldType(DateEditFieldComponent, 'date', ['Date'])
.addFieldType(FormattableEditField, 'wiki-textarea', ['Formattable']); .addFieldType(FormattableEditFieldComponent, 'wiki-textarea', ['Formattable'])
.addFieldType(WorkPackageCommentFieldComponent, '_comment', ['comment']);
}; };
} }

@ -27,41 +27,17 @@
// ++ // ++
import {Injectable, Injector} from '@angular/core'; import {Injectable, Injector} from '@angular/core';
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {AbstractFieldService, IFieldType} from "core-app/modules/fields/field.service"; import {AbstractFieldService, IFieldType} from "core-app/modules/fields/field.service";
import {IFieldSchema} from "core-app/modules/fields/field.base"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {DisplayField} from "core-app/modules/fields/display/display-field.module";
import {DisplayFieldContext, IDisplayFieldType} from "core-app/modules/fields/display/display-field.service";
export interface IEditFieldType extends IFieldType<EditField> { export interface IEditFieldType extends IFieldType<EditFieldComponent> {
new(resource:WorkPackageResource, attributeType:string, schema:IFieldSchema):EditField; new():EditFieldComponent;
} }
@Injectable() @Injectable()
export class EditFieldService extends AbstractFieldService<EditField, IEditFieldType> { export class EditFieldService extends AbstractFieldService<EditFieldComponent, IEditFieldType> {
constructor(injector:Injector) { constructor(injector:Injector) {
super(injector); super(injector);
} }
/**
* Create an instance of the field type given the required arguments
* with either in descending order:
*
* 1. The registered field name (most specific)
* 2. The registered field for the schema attribute type
* 3. The default field type
*
* @param resource
* @param {string} fieldName
* @param {IFieldSchema} schema
* @returns {T}
*/
public getField(resource:any, fieldName:string, schema:IFieldSchema):EditField {
let type = this.fieldType(fieldName) || this.fieldType(schema.type) || this.defaultFieldType;
let fieldClass:IEditFieldType = this.classes[type];
return new fieldClass(resource, fieldName, schema);
}
} }

@ -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;
}
}

@ -26,9 +26,9 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; import {Subject} from 'rxjs';
export interface IEditFieldHandler { export abstract class EditFieldHandler {
/** /**
* Whether the handler belongs to a larger edit mode form * Whether the handler belongs to a larger edit mode form
* e.g., WP-create * e.g., WP-create
@ -38,11 +38,20 @@ export interface IEditFieldHandler {
/** Whether the field is currently active */ /** Whether the field is currently active */
active:boolean; active:boolean;
/** Whether the field is being saved */
inFlight:boolean;
/** /**
* Return a unique ID for this edit field * Return a unique ID for this edit field
*/ */
htmlId:string; htmlId:string;
/**
* The name of the attribute
*/
fieldName:string;
/** /**
* Accessibility label for the field * Accessibility label for the field
*/ */
@ -54,25 +63,39 @@ export interface IEditFieldHandler {
errorMessageOnLabel?:string; errorMessageOnLabel?:string;
/** /**
* Project resource * On destroy observable
*/ */
project:ProjectResource; public onDestroy = new Subject<void>();
// OnSubmit callbacks that may register from fields
protected _onSubmitHandlers:Array<() => Promise<void>> = [];
/**
* Call field submission callback handlers
*/
public onSubmit():Promise<any> {
return Promise.all(this._onSubmitHandlers.map((cb) => cb()));
}
public registerOnSubmit(callback:() => Promise<void>) {
this._onSubmitHandlers.push(callback);
}
/** /**
* Stop event propagation * Stop event propagation
*/ */
stopPropagation(evt:JQueryEventObject):boolean; public abstract stopPropagation(evt:JQueryEventObject):boolean;
/** /**
* Focus on the active field. * Focus on the active field.
* Optionally, try to set the click position to the given offset if the field is an input element. * Optionally, try to set the click position to the given offset if the field is an input element.
*/ */
focus(setClickOffset?:number):void; public abstract focus(setClickOffset?:number):void;
/** /**
* Handle a user submitting the field (e.g, ng-change) * Handle a user submitting the field (e.g, ng-change)
*/ */
handleUserSubmit():Promise<any>; public abstract handleUserSubmit():Promise<any>;
/** /**
* Handle users pressing enter inside an edit mode. * Handle users pressing enter inside an edit mode.
@ -80,25 +103,25 @@ export interface IEditFieldHandler {
* In an edit mode, we can't derive from a submit event wheteher the user pressed enter * In an edit mode, we can't derive from a submit event wheteher the user pressed enter
* (and on what field he did that). * (and on what field he did that).
*/ */
handleUserKeydown(event:JQueryEventObject, onlyCancel?:boolean):void; public abstract handleUserKeydown(event:JQueryEventObject, onlyCancel?:boolean):void;
/** /**
* Cancel edit * Cancel edit
*/ */
handleUserCancel():void; public abstract handleUserCancel():void;
/** /**
* Cancel any pending changes * Cancel any pending changes
*/ */
reset():void; public abstract reset():void;
/** /**
* Close the field, resetting it with its display value. * Close the field, resetting it with its display value.
*/ */
deactivate(focus:boolean):void; public abstract deactivate(focus:boolean):void;
/** /**
* Returns whether the field has been changed * Returns whether the field has been changed
*/ */
isChanged():boolean; public abstract isChanged():boolean;
} }

@ -1,5 +1,5 @@
<div *ngIf="handler" <div *ngIf="handler"
class="wp-inline-edit--active-field wp-edit-field inplace-edit {{ editField.name }}" class="wp-inline-edit--active-field wp-edit-field inplace-edit {{ handler.fieldName }}"
[ngClass]="{'-error': handler.isErrorenous }"> [ngClass]="{'-error': handler.isErrorenous }">
<form (submit)="handler.handleUserSubmit()" <form (submit)="handler.handleUserSubmit()"
role="form" role="form"
@ -11,6 +11,6 @@
{{handler.errorMessageOnLabel}} {{handler.errorMessageOnLabel}}
</label> </label>
<ng-container *ngComponentOutlet="editField.component; injector: fieldInjector"></ng-container> <ng-container *ngComponentOutlet="componentClass; injector: fieldInjector"></ng-container>
</form> </form>
</div> </div>

@ -1,52 +1,64 @@
import { import {
AfterViewInit, AfterViewInit,
EventEmitter,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
Injector, Injector,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output Output
} from "@angular/core"; } from "@angular/core";
import {EditField} from "core-app/modules/fields/edit/edit.field.module"; import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler";
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface";
import { import {
OpEditingPortalFieldToken, EditFieldComponent,
OpEditingPortalHandlerToken OpEditingPortalChangesetToken,
OpEditingPortalHandlerToken,
OpEditingPortalSchemaToken
} from "core-app/modules/fields/edit/edit-field.component"; } from "core-app/modules/fields/edit/edit-field.component";
import {createLocalInjector} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.injector"; import {createLocalInjector} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.injector";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
import {EditFieldService, IEditFieldType} from "core-app/modules/fields/edit/edit-field.service";
@Component({ @Component({
selector: 'edit-form-portal', selector: 'edit-form-portal',
templateUrl: './edit-form-portal.component.html' templateUrl: './edit-form-portal.component.html'
}) })
export class EditFormPortalComponent implements OnInit, OnDestroy, AfterViewInit { export class EditFormPortalComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() editFieldInput:EditField; @Input() schemaInput:IFieldSchema;
@Input() editFieldHandler:IEditFieldHandler; @Input() changesetInput:WorkPackageChangeset;
@Input() editFieldHandler:EditFieldHandler;
@Output() public onEditFieldReady = new EventEmitter<void>(); @Output() public onEditFieldReady = new EventEmitter<void>();
public handler:IEditFieldHandler; public handler:EditFieldHandler;
public editField:EditField; public schema:IFieldSchema;
public changeset:WorkPackageChangeset;
public fieldInjector:Injector; public fieldInjector:Injector;
public componentClass:IEditFieldType;
public htmlId:string; public htmlId:string;
public label:string; public label:string;
constructor(readonly injector:Injector, constructor(readonly injector:Injector,
readonly editField:EditFieldService,
readonly elementRef:ElementRef) { readonly elementRef:ElementRef) {
} }
ngOnInit() { ngOnInit() {
if (this.editFieldHandler && this.editFieldInput) { if (this.editFieldHandler && this.schemaInput) {
this.handler = this.editFieldHandler; this.handler = this.editFieldHandler;
this.editField = this.editFieldInput; this.schema = this.schemaInput;
this.changeset = this.changesetInput;
} else { } else {
this.handler = this.injector.get<IEditFieldHandler>(OpEditingPortalHandlerToken); this.handler = this.injector.get<EditFieldHandler>(OpEditingPortalHandlerToken);
this.editField = this.injector.get<EditField>(OpEditingPortalFieldToken); this.schema = this.injector.get<IFieldSchema>(OpEditingPortalSchemaToken);
this.changeset = this.injector.get<WorkPackageChangeset>(OpEditingPortalChangesetToken);
} }
this.fieldInjector = createLocalInjector(this.injector, this.handler, this.editField); this.componentClass = this.editField.getClassFor(this.handler.fieldName, this.schema.type);
this.fieldInjector = createLocalInjector(this.injector, this.changeset, this.handler, this.schema);
} }
ngOnDestroy() { ngOnDestroy() {
@ -56,8 +68,6 @@ export class EditFormPortalComponent implements OnInit, OnDestroy, AfterViewInit
ngAfterViewInit() { ngAfterViewInit() {
// Fire in a timeout to avoid same execution context in AfterViewInit // Fire in a timeout to avoid same execution context in AfterViewInit
setTimeout(() => { setTimeout(() => {
// Call $onInit once the field is ready
this.editField.$onInit(this.elementRef.nativeElement);
this.onEditFieldReady.emit(); this.onEditFieldReady.emit();
}); });
} }

@ -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);
} }

@ -5,10 +5,10 @@ import {WorkPackageEditFieldHandler} from "core-components/wp-edit-form/work-pac
import {WorkPackageEditForm} from "core-components/wp-edit-form/work-package-edit-form"; import {WorkPackageEditForm} from "core-components/wp-edit-form/work-package-edit-form";
import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector} from "@angular/core"; import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector} from "@angular/core";
import {ComponentPortal, DomPortalOutlet} from "@angular/cdk/portal"; import {ComponentPortal, DomPortalOutlet} from "@angular/cdk/portal";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {EditFormPortalComponent} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.component"; import {EditFormPortalComponent} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.component";
import {createLocalInjector} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.injector"; import {createLocalInjector} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.injector";
import {take} from "rxjs/operators"; import {take} from "rxjs/operators";
import {IFieldSchema} from "core-app/modules/fields/field.base";
@Injectable() @Injectable()
export class WorkPackageEditingPortalService { export class WorkPackageEditingPortalService {
@ -21,7 +21,7 @@ export class WorkPackageEditingPortalService {
public create(container:HTMLElement, public create(container:HTMLElement,
form:WorkPackageEditForm, form:WorkPackageEditForm,
field:EditField, schema:IFieldSchema,
fieldName:string, fieldName:string,
errors:string[]):Promise<WorkPackageEditFieldHandler> { errors:string[]):Promise<WorkPackageEditFieldHandler> {
@ -33,14 +33,19 @@ export class WorkPackageEditingPortalService {
this.injector, this.injector,
form, form,
fieldName, fieldName,
field, schema,
container, container,
() => outlet.detach(), // Don't call .dispose() on the outlet, it destroys the DOM element
errors errors
); );
fieldHandler
.onDestroy
.pipe(take(1))
// Don't call .dispose() on the outlet, it destroys the DOM element
.subscribe(() => outlet.detach());
// Create an injector that contains injectable reference to the edit field and handler // Create an injector that contains injectable reference to the edit field and handler
const injector = createLocalInjector(this.injector, fieldHandler, field); const injector = createLocalInjector(this.injector, form.changeset, fieldHandler, schema);
// Create a portal for the edit-form/field // Create a portal for the edit-form/field
const portal = new ComponentPortal(EditFormPortalComponent, null, injector); const portal = new ComponentPortal(EditFormPortalComponent, null, injector);

@ -1,5 +1,5 @@
<div class="inplace-edit--dashboard"> <div class="inplace-edit--dashboard">
<div class="inplace-edit--controls" *ngIf="fieldController.active"> <div class="inplace-edit--controls" *ngIf="field.handler.active">
<accessible-by-keyboard (execute)="save()" <accessible-by-keyboard (execute)="save()"
[attr.disabled]="(field.required && field.isEmpty()) || undefined" [attr.disabled]="(field.required && field.isEmpty()) || undefined"
[linkTitle]="saveTitle" [linkTitle]="saveTitle"

@ -26,8 +26,8 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {Component, Input, Output, EventEmitter} from "@angular/core"; import {Component, Input, Output, EventEmitter} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
@Component({ @Component({
selector: 'edit-field-controls', selector: 'edit-field-controls',
@ -36,14 +36,10 @@ import {Component, Input, Output, EventEmitter} from "@angular/core";
export class EditFieldControlsComponent { export class EditFieldControlsComponent {
@Input() public cancelTitle:string; @Input() public cancelTitle:string;
@Input() public saveTitle:string; @Input() public saveTitle:string;
@Input() public fieldController:any; @Input('fieldController') public field:EditFieldComponent;
@Output() public onSave = new EventEmitter<void>(); @Output() public onSave = new EventEmitter<void>();
@Output() public onCancel = new EventEmitter<void>(); @Output() public onCancel = new EventEmitter<void>();
public get field():EditField {
return this.fieldController.field;
}
public save() { public save() {
this.onSave.emit(); this.onSave.emit();
} }

@ -28,30 +28,23 @@
import {Component} from "@angular/core"; import {Component} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
@Component({ @Component({
template: ` template: `
<input type="checkbox" <input type="checkbox"
class="wp-inline-edit--field wp-inline-edit--boolean-field" class="wp-inline-edit--field wp-inline-edit--boolean-field"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[checked]="field.value" [checked]="value"
(change)="updateValue(!field.value)" (change)="updateValue(!value)"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[disabled]="field.inFlight" [disabled]="inFlight"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
` `
}) })
export class BooleanEditFieldComponent extends EditFieldComponent { export class BooleanEditFieldComponent extends EditFieldComponent {
public field:BooleanEditField;
public updateValue(newValue:boolean) { public updateValue(newValue:boolean) {
this.field.value = newValue; this.value = newValue;
this.handler.handleUserSubmit(); this.handler.handleUserSubmit();
} }
} }
export class BooleanEditField extends EditField {
public component = BooleanEditFieldComponent;
}

@ -30,23 +30,22 @@ import {Component} from "@angular/core";
import * as moment from "moment"; import * as moment from "moment";
import {TimezoneService} from "core-components/datetime/timezone.service"; import {TimezoneService} from "core-components/datetime/timezone.service";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
@Component({ @Component({
template: ` template: `
<op-date-picker <op-date-picker
tabindex="-1" tabindex="-1"
(onChange)="onValueSelected($event)" (onChange)="onValueSelected($event)"
[initialDate]="field.defaultDate"> [initialDate]="defaultDate">
<input [ngModel]="formatter(field.value)" <input [ngModel]="formatter(value)"
(ngModelChange)="field.value = parser($event);" (ngModelChange)="value = parser($event);"
type="text" type="text"
class="wp-inline-edit--field" class="wp-inline-edit--field"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[attr.required]="field.required" [attr.required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[attr.placeholder]="field.placeholder" [attr.placeholder]="placeholder"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
</op-date-picker> </op-date-picker>
@ -54,11 +53,10 @@ import {EditField} from "core-app/modules/fields/edit/edit.field.module";
` `
}) })
export class DateEditFieldComponent extends EditFieldComponent { export class DateEditFieldComponent extends EditFieldComponent {
public field:DateEditField;
readonly timezoneService = this.injector.get(TimezoneService); readonly timezoneService = this.injector.get(TimezoneService);
public onValueSelected(data:string) { public onValueSelected(data:string) {
this.field.value = this.parser(data); this.value = this.parser(data);
this.handler.handleUserSubmit(); this.handler.handleUserSubmit();
} }
@ -78,10 +76,6 @@ export class DateEditFieldComponent extends EditFieldComponent {
return null; return null;
} }
} }
}
export class DateEditField extends EditField {
public component = DateEditFieldComponent;
/** /**
* Return the default date for the datepicker instance. * Return the default date for the datepicker instance.

@ -29,7 +29,6 @@
import {TimezoneService} from 'core-components/datetime/timezone.service'; import {TimezoneService} from 'core-components/datetime/timezone.service';
import * as moment from 'moment'; import * as moment from 'moment';
import {Component} from "@angular/core"; import {Component} from "@angular/core";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
@Component({ @Component({
@ -37,17 +36,17 @@ import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.compon
<input type="number" <input type="number"
step="0.01" step="0.01"
class="wp-inline-edit--field" class="wp-inline-edit--field"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[ngModel]="formatter(field.value)" [ngModel]="formatter(value)"
(ngModelChange)="field.value = parser($event)" (ngModelChange)="value = parser($event)"
[attr.required]="field.required" [attr.required]="required"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[disabled]="field.inFlight" [disabled]="inFlight"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
` `
}) })
export class DurationEditFieldComponent extends EditFieldComponent { export class DurationEditFieldComponent extends EditFieldComponent {
public field:DurationEditField; readonly TimezoneService:TimezoneService = this.$injector.get(TimezoneService);
public parser(value:any) { public parser(value:any) {
if (!isNaN(value)) { if (!isNaN(value)) {
@ -61,14 +60,9 @@ export class DurationEditFieldComponent extends EditFieldComponent {
public formatter(value:any) { public formatter(value:any) {
return Number(moment.duration(value).asHours().toFixed(2)); return Number(moment.duration(value).asHours().toFixed(2));
} }
}
export class DurationEditField extends EditField {
public component = DurationEditFieldComponent;
readonly TimezoneService:TimezoneService = this.$injector.get(TimezoneService);
protected parseValue(val:moment.Moment | null) { protected parseValue(val:moment.Moment | null) {
return val === null ? null : val.toISOString(); return val === null ? null : val.toISOString();
} }
} }

@ -26,7 +26,6 @@
// ++ // ++
import {Component} from "@angular/core"; import {Component} from "@angular/core";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
@Component({ @Component({
@ -34,10 +33,10 @@ import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.compon
<input type="number" <input type="number"
step="any" step="any"
class="wp-inline-edit--field" class="wp-inline-edit--field"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[attr.required]="field.required" [attr.required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[(ngModel)]="field.value" [(ngModel)]="value"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[attr.lang]="locale" [attr.lang]="locale"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
@ -45,9 +44,4 @@ import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.compon
}) })
export class FloatEditFieldComponent extends EditFieldComponent { export class FloatEditFieldComponent extends EditFieldComponent {
public locale = I18n.locale; public locale = I18n.locale;
public field:FloatEditField;
}
export class FloatEditField extends EditField {
public component = FloatEditFieldComponent;
} }

@ -25,49 +25,54 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {Component, ViewChild} from "@angular/core"; import {Component, OnInit, ViewChild} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {FormattableEditField} from "core-app/modules/fields/edit/field-types/formattable-edit-field";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {ICKEditorContext, ICKEditorInstance} from "core-app/modules/common/ckeditor/ckeditor-setup.service"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
import {OpCkeditorComponent} from "core-app/modules/common/ckeditor/op-ckeditor.component"; import {OpCkeditorComponent} from "core-app/modules/common/ckeditor/op-ckeditor.component";
import {ICKEditorContext, ICKEditorInstance} from "core-app/modules/common/ckeditor/ckeditor-setup.service";
import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({ export const formattableFieldTemplate = `
template: `
<div class="textarea-wrapper"> <div class="textarea-wrapper">
<div class="op-ckeditor--wrapper op-ckeditor-element"> <div class="op-ckeditor--wrapper op-ckeditor-element">
<op-ckeditor [context]="context" <op-ckeditor [context]="ckEditorContext"
[content]="field.rawValue || ''" [content]="rawValue"
(onContentChange)="onContentChange($event)" (onContentChange)="onContentChange($event)"
(onInitialized)="onCkeditorSetup($event)" (onInitialized)="onCkeditorSetup($event)"
[ckEditorType]="editorType"> [ckEditorType]="editorType">
</op-ckeditor> </op-ckeditor>
</div> </div>
<edit-field-controls *ngIf="!handler.inEditMode" <edit-field-controls *ngIf="!handler.inEditMode"
[fieldController]="handler" [fieldController]="field"
(onSave)="handleUserSubmit()" (onSave)="handler.handleUserSubmit()"
(onCancel)="handler.handleUserCancel()" (onCancel)="handler.handleUserCancel()"
[saveTitle]="field.text.save" [saveTitle]="text.save"
[cancelTitle]="field.text.cancel"> [cancelTitle]="text.cancel">
</edit-field-controls> </edit-field-controls>
</div> </div>
` `
@Component({
template: formattableFieldTemplate
}) })
export class FormattableEditFieldComponent extends EditFieldComponent { export class FormattableEditFieldComponent extends EditFieldComponent implements OnInit {
public field:FormattableEditField; readonly pathHelper:PathHelperService = this.$injector.get(PathHelperService);
private readonly pathHelper:PathHelperService = this.injector.get(PathHelperService);
private readonly Notifications = this.injector.get(NotificationsService); public readonly field = this;
@ViewChild(OpCkeditorComponent) instance:OpCkeditorComponent; @ViewChild(OpCkeditorComponent) instance:OpCkeditorComponent;
public initialize() { // Values used in template
super.initialize(); public isPreview:boolean = false;
this.field.instance = this; 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 onContentChange(value:string) { ngOnInit() {
this.field.rawValue = value; this.handler.registerOnSubmit(() => this.getCurrentValue());
} }
public onCkeditorSetup(editor:ICKEditorInstance) { public onCkeditorSetup(editor:ICKEditorInstance) {
@ -76,21 +81,28 @@ export class FormattableEditFieldComponent extends EditFieldComponent {
} }
} }
public getCurrentValue() { public getCurrentValue():Promise<void> {
return this.instance.getTransformedContent(); return this.instance
.getTransformedContent()
.then((val) => {
this.rawValue = val;
});
}
public onContentChange(value:string) {
this.rawValue = value;
} }
public handleUserSubmit() { public handleUserSubmit() {
this.getCurrentValue() this.getCurrentValue()
.then((value:string) => { .then(() => {
this.field.rawValue = value;
this.handler.handleUserSubmit(); this.handler.handleUserSubmit();
}); });
return false; return false;
} }
public get context():ICKEditorContext { public get ckEditorContext():ICKEditorContext {
return { return {
resource: this.resource, resource: this.resource,
macros: 'none' as 'none', macros: 'none' as 'none',
@ -99,18 +111,44 @@ export class FormattableEditFieldComponent extends EditFieldComponent {
} }
public get editorType() { public get editorType() {
if (this.field.name === 'description') { if (this.name === 'description') {
return 'full'; return 'full';
} else { } else {
return 'constrained'; return 'constrained';
} }
} }
public get previewContext() { private get previewContext() {
if (this.resource.isNew && this.resource.project) { if (this.resource.isNew && this.resource.project) {
return this.resource.project.href; return this.resource.project.href;
} else if (!this.resource.isNew) { } else if (!this.resource.isNew) {
return this.pathHelper.api.v3.work_packages.id(this.resource.id).path; return this.pathHelper.api.v3.work_packages.id(this.resource.id).path;
} }
} }
public reset() {
if (this.instance) {
this.instance.content = this.rawValue;
}
}
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 isEmpty():boolean {
return !(this.value && this.value.raw);
}
public get isFormattable() {
return true;
}
} }

@ -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();
}
});
}
}

@ -25,29 +25,22 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {DurationEditField} from "core-app/modules/fields/edit/field-types/duration-edit-field";
import {Component} from "@angular/core"; import {Component} from "@angular/core";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
@Component({ @Component({
template: ` template: `
<input type="number" <input type="number"
class="wp-inline-edit--field" class="wp-inline-edit--field"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[attr.required]="field.required" [attr.required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[attr.lang]="locale" [attr.lang]="locale"
[(ngModel)]="field.value" [(ngModel)]="value"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
` `
}) })
export class IntegerEditFieldComponent extends EditFieldComponent { export class IntegerEditFieldComponent extends EditFieldComponent {
public locale = I18n.locale; public locale = I18n.locale;
public field:DurationEditField;
}
export class IntegerEditField extends EditField {
public component = IntegerEditFieldComponent;
} }

@ -4,9 +4,9 @@
*ngIf="!isMultiselect" *ngIf="!isMultiselect"
class="focus-input wp-inline-edit--field inplace-edit--field -animated form--select" class="focus-input wp-inline-edit--field inplace-edit--field -animated form--select"
[(ngModel)]="selectedOption" [(ngModel)]="selectedOption"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[required]="field.required" [required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[id]="handler.htmlId" [id]="handler.htmlId"
(keydown)="handler.handleUserKeydown($event, true)" (keydown)="handler.handleUserKeydown($event, true)"
(change)="handler.handleUserSubmit()" (change)="handler.handleUserSubmit()"
@ -32,9 +32,9 @@
*ngIf="isMultiselect" *ngIf="isMultiselect"
[(ngModel)]="selectedOption" [(ngModel)]="selectedOption"
class="focus-input wp-inline-edit--field inplace-edit--textarea -animated form--select" class="focus-input wp-inline-edit--field inplace-edit--textarea -animated form--select"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[required]="field.required" [required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[id]="handler.htmlId" [id]="handler.htmlId"
(keydown)="handler.handleUserKeydown($event, true)" (keydown)="handler.handleUserKeydown($event, true)"
(change)="submitOnSingleSelect()" (change)="submitOnSingleSelect()"
@ -68,7 +68,7 @@
</a> </a>
</div> </div>
<edit-field-controls [fieldController]="handler" <edit-field-controls [fieldController]="self"
*ngIf="isMultiselect && !handler.inEditMode" *ngIf="isMultiselect && !handler.inEditMode"
(onSave)="handler.handleUserSubmit()" (onSave)="handler.handleUserSubmit()"
(onCancel)="handler.handleUserCancel()" (onCancel)="handler.handleUserCancel()"

@ -31,8 +31,7 @@ import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {Component, OnInit} from "@angular/core"; import {Component, OnInit} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {ValueOption} from "core-app/modules/fields/edit/field-types/select-edit-field"; import {ValueOption} from "core-app/modules/fields/edit/field-types/select-edit-field.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
@Component({ @Component({
templateUrl: './multi-select-edit-field.component.html' templateUrl: './multi-select-edit-field.component.html'
@ -215,7 +214,3 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
} }
} }
} }
export class MultiSelectEditField extends EditField {
component = MultiSelectEditFieldComponent;
}

@ -1,9 +1,9 @@
<select [(ngModel)]="selectedOption" <select [(ngModel)]="selectedOption"
[compareWith]="compareByHref" [compareWith]="compareByHref"
class="wp-inline-edit--field form--select" class="wp-inline-edit--field form--select"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[required]="field.required" [required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[id]="handler.htmlId" [id]="handler.htmlId"
(keydown)="handler.handleUserKeydown($event, true)" (keydown)="handler.handleUserKeydown($event, true)"
(change)="handler.handleUserSubmit()" (change)="handler.handleUserSubmit()"

@ -30,10 +30,6 @@ import {Component} from "@angular/core";
import {HalResourceSortingService} from "core-app/modules/hal/services/hal-resource-sorting.service"; import {HalResourceSortingService} from "core-app/modules/hal/services/hal-resource-sorting.service";
import {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; import {CollectionResource} from "core-app/modules/hal/resources/collection-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {WorkPackageEditingService} from "../../../../components/wp-edit-form/work-package-editing-service";
import {componentDestroyed} from "ng2-rx-componentdestroyed";
import {takeUntil} from "rxjs/internal/operators";
import {EditFieldComponent} from "../edit-field.component"; import {EditFieldComponent} from "../edit-field.component";
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions"; import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
@ -74,7 +70,7 @@ export class SelectEditFieldComponent extends EditFieldComponent {
} }
public get selectedOption() { public get selectedOption() {
const href = this.field.value ? this.value.$href : null; const href = this.value ? this.value.$href : null;
return _.find(this.valueOptions, o => o.$href === href)!; return _.find(this.valueOptions, o => o.$href === href)!;
} }
@ -123,7 +119,3 @@ export class SelectEditFieldComponent extends EditFieldComponent {
} }
} }
} }
export class SelectEditField extends EditField {
component = SelectEditFieldComponent;
}

@ -28,26 +28,20 @@
import {Component} from "@angular/core"; import {Component} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
@Component({ @Component({
template: ` template: `
<input type="text" <input type="text"
class="wp-inline-edit--field" class="wp-inline-edit--field"
[focus]="shouldFocus" [focus]="shouldFocus"
[attr.aria-required]="field.required" [attr.aria-required]="required"
[attr.required]="field.required" [attr.required]="required"
[disabled]="field.inFlight" [disabled]="inFlight"
[(ngModel)]="value" [(ngModel)]="value"
(keydown)="handler.handleUserKeydown($event)" (keydown)="handler.handleUserKeydown($event)"
[id]="handler.htmlId" /> [id]="handler.htmlId" />
` `
}) })
export class TextEditFieldComponent extends EditFieldComponent { export class TextEditFieldComponent extends EditFieldComponent {
public field:TextEditField; public shouldFocus = this.name === 'subject';
public shouldFocus = this.field.name === 'subject';
}
export class TextEditField extends EditField {
public component = TextEditFieldComponent;
} }

@ -26,11 +26,17 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {TextEditField} from "core-app/modules/fields/edit/field-types/text-edit-field"; import {Component} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
export class WorkPackageEditField extends TextEditField { @Component({
template: `
public get writable() { <input type="text"
return false; class="wp-inline-edit--field"
} disabled
[ngModel]="value"
[id]="handler.htmlId" />
`
})
export class WorkPackageEditFieldComponent extends EditFieldComponent {
} }

@ -27,12 +27,12 @@
// ++ // ++
import {Injector} from '@angular/core'; import {Injector} from '@angular/core';
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {DisplayFieldContext} from "core-app/modules/fields/display/display-field.service";
export interface IFieldSchema { export interface IFieldSchema {
type:string; type:string;
writable:boolean; writable:boolean;
allowedValues:any; allowedValues?:any;
required?:boolean; required?:boolean;
hasDefault:boolean; hasDefault:boolean;
name?:string; name?:string;
@ -41,6 +41,10 @@ export interface IFieldSchema {
export class Field { export class Field {
public static type:string; public static type:string;
public static $injector:Injector; public static $injector:Injector;
public resource:any;
public name:string;
public schema:IFieldSchema;
public context:DisplayFieldContext;
public get displayName():string { public get displayName():string {
return this.schema.name || this.name; return this.schema.name || this.name;
@ -77,16 +81,4 @@ export class Field {
protected get $injector():Injector { protected get $injector():Injector {
return (this.constructor as typeof Field).$injector; return (this.constructor as typeof Field).$injector;
} }
protected initializer() {
}
protected I18n:I18nService
constructor(public resource:any,
public name:string,
public schema:IFieldSchema) {
this.I18n = this.$injector.get(I18nService);
this.initializer();
}
} }

@ -65,8 +65,9 @@ export abstract class AbstractFieldService<T extends Field, C extends IFieldType
* @param {string} fieldName * @param {string} fieldName
* @returns {C} * @returns {C}
*/ */
public getClassFor(fieldName:string):C { public getClassFor(fieldName:string, type:string = 'unknown'):C {
return this.classes[fieldName] || this.classes[this.defaultFieldType]; let key = this.fieldType(fieldName) || this.fieldType(type) || this.defaultFieldType;
return this.classes[key];
} }
/** /**
@ -96,7 +97,7 @@ export abstract class AbstractFieldService<T extends Field, C extends IFieldType
* @returns {this} * @returns {this}
*/ */
public extendFieldType(fieldType:string, attributes:string[]) { public extendFieldType(fieldType:string, attributes:string[]) {
let fieldClass = this.getClassFor(fieldType); let fieldClass = this.classes[fieldType] || this.getClassFor(fieldType);
return this.addFieldType(fieldClass, fieldType, attributes); return this.addFieldType(fieldClass, fieldType, attributes);
} }

@ -33,21 +33,22 @@ import {DisplayFieldService} from "core-app/modules/fields/display/display-field
import {initializeCoreEditFields} from "core-app/modules/fields/edit/edit-field.initializer"; import {initializeCoreEditFields} from "core-app/modules/fields/edit/edit-field.initializer";
import {initializeCoreDisplayFields} from "core-app/modules/fields/display/display-field.initializer"; import {initializeCoreDisplayFields} from "core-app/modules/fields/display/display-field.initializer";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component"; import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {BooleanEditFieldComponent} from "core-app/modules/fields/edit/field-types/boolean-edit-field"; import {BooleanEditFieldComponent} from "core-app/modules/fields/edit/field-types/boolean-edit-field.component";
import {DateEditFieldComponent} from "core-app/modules/fields/edit/field-types/date-edit-field"; import {DateEditFieldComponent} from "core-app/modules/fields/edit/field-types/date-edit-field.component";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {DurationEditFieldComponent} from "core-app/modules/fields/edit/field-types/duration-edit-field"; import {DurationEditFieldComponent} from "core-app/modules/fields/edit/field-types/duration-edit-field.component";
import {FloatEditFieldComponent} from "core-app/modules/fields/edit/field-types/float-edit-field"; import {FloatEditFieldComponent} from "core-app/modules/fields/edit/field-types/float-edit-field.component";
import {IntegerEditFieldComponent} from "core-app/modules/fields/edit/field-types/integer-edit-field"; import {IntegerEditFieldComponent} from "core-app/modules/fields/edit/field-types/integer-edit-field.component";
import {MultiSelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/multi-select-edit-field"; import {MultiSelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/multi-select-edit-field.component";
import {SelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/select-edit-field"; import {SelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/select-edit-field.component";
import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component"; import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
import {TextEditFieldComponent} from "core-app/modules/fields/edit/field-types/text-edit-field"; import {TextEditFieldComponent} from "core-app/modules/fields/edit/field-types/text-edit-field.component";
import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module"; import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module";
import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service";
import {EditFormPortalComponent} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.component"; import {EditFormPortalComponent} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.component";
import {EditFieldControlsComponent,} from "core-app/modules/fields/edit/field-controls/edit-field-controls.component"; import {EditFieldControlsComponent,} from "core-app/modules/fields/edit/field-controls/edit-field-controls.component";
import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-a11y.module"; import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-a11y.module";
import {WorkPackageEditFieldComponent} from "core-app/modules/fields/edit/field-types/work-package-edit-field.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -80,6 +81,7 @@ import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-
SelectEditFieldComponent, SelectEditFieldComponent,
TextEditFieldComponent, TextEditFieldComponent,
EditFieldControlsComponent, EditFieldControlsComponent,
WorkPackageEditFieldComponent,
], ],
entryComponents: [ entryComponents: [
EditFormPortalComponent, EditFormPortalComponent,
@ -93,6 +95,7 @@ import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-
MultiSelectEditFieldComponent, MultiSelectEditFieldComponent,
SelectEditFieldComponent, SelectEditFieldComponent,
TextEditFieldComponent, TextEditFieldComponent,
WorkPackageEditFieldComponent,
] ]
}) })
export class OpenprojectFieldsModule { } export class OpenprojectFieldsModule { }

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {APP_INITIALIZER, NgModule} from '@angular/core'; import {APP_INITIALIZER, ErrorHandler, NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import { import {
@ -45,6 +45,7 @@ import {OpenProjectHeaderInterceptor} from 'core-app/modules/hal/http/openprojec
import {UserDmService} from 'core-app/modules/hal/dm-services/user-dm.service'; import {UserDmService} from 'core-app/modules/hal/dm-services/user-dm.service';
import {ProjectDmService} from 'core-app/modules/hal/dm-services/project-dm.service'; import {ProjectDmService} from 'core-app/modules/hal/dm-services/project-dm.service';
import {HalResourceSortingService} from "core-app/modules/hal/services/hal-resource-sorting.service"; import {HalResourceSortingService} from "core-app/modules/hal/services/hal-resource-sorting.service";
import {HalAwareErrorHandler} from "core-app/modules/hal/services/hal-aware-error-handler";
@NgModule({ @NgModule({
imports: [ imports: [
@ -52,6 +53,7 @@ import {HalResourceSortingService} from "core-app/modules/hal/services/hal-resou
HttpClientModule, HttpClientModule,
], ],
providers: [ providers: [
{ provide: ErrorHandler, useClass: HalAwareErrorHandler },
HalResourceService, HalResourceService,
HalResourceSortingService, HalResourceSortingService,
{ provide: HTTP_INTERCEPTORS, useClass: OpenProjectHeaderInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: OpenProjectHeaderInterceptor, multi: true },

@ -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);
}
}

@ -108,6 +108,9 @@ describe 'Upload attachment to work package', js: true do
target = find('.ck-content') target = find('.ck-content')
attachments.drag_and_drop_file(target, image_fixture) attachments.drag_and_drop_file(target, image_fixture)
sleep 2
expect(page).not_to have_selector('notification-upload-progress')
editor.in_editor do |container, editable| editor.in_editor do |container, editable|
expect(editable).to have_selector('img[src*="/api/v3/attachments/"]', wait: 20) expect(editable).to have_selector('img[src*="/api/v3/attachments/"]', wait: 20)
end end
@ -118,8 +121,6 @@ describe 'Upload attachment to work package', js: true do
caption.click caption.click
caption.base.send_keys('Some image caption') caption.base.send_keys('Some image caption')
expect(page).not_to have_selector('notification-upload-progress')
click_on 'Save' click_on 'Save'
wp_page.expect_notification( wp_page.expect_notification(
@ -127,6 +128,7 @@ describe 'Upload attachment to work package', js: true do
) )
field = wp_page.edit_field :description field = wp_page.edit_field :description
expect(field.display_element).to have_selector('img') expect(field.display_element).to have_selector('img')
expect(field.display_element).to have_content('Some image caption') expect(field.display_element).to have_content('Some image caption')

@ -114,7 +114,7 @@ describe 'edit work package', js: true do
dueDate: '2013-03-20', dueDate: '2013-03-20',
responsible: manager.name, responsible: manager.name,
assignee: manager.name, assignee: manager.name,
estimatedTime: '5.00', estimatedTime: '5',
priority: priority2.name, priority: priority2.name,
version: version.name, version: version.name,
category: category.name, category: category.name,

@ -184,8 +184,8 @@ describe 'new work package', js: true do
find(".customField#{ids.last} option", text: 'foo').select_option find(".customField#{ids.last} option", text: 'foo').select_option
save_work_package!(false) save_work_package!(false)
# Its a known bug that custom fields validation errors do not contain their names
notification.expect_error("can't be blank.") notification.expect_error("#{custom_field1.name} can't be blank.")
cf1.set 'Custom field content' cf1.set 'Custom field content'
save_work_package!(true) save_work_package!(true)

Loading…
Cancel
Save