[29390] Introduce new work package change and commit

pull/7677/head
Oliver Günther 6 years ago
parent 3726e22476
commit e23ee0c669
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 4
      frontend/src/app/components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts
  2. 3
      frontend/src/app/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts
  3. 6
      frontend/src/app/components/op-context-menu/handlers/op-types-context-menu.directive.ts
  4. 7
      frontend/src/app/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts
  5. 24
      frontend/src/app/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts
  6. 4
      frontend/src/app/components/work-packages/work-package-cache.service.spec.ts
  7. 26
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment-field-handler.ts
  8. 2
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html
  9. 15
      frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts
  10. 1
      frontend/src/app/components/work-packages/wp-relations-count/wp-watchers-count.component.ts
  11. 79
      frontend/src/app/components/work-packages/wp-single-view/wp-single-view.component.ts
  12. 4
      frontend/src/app/components/work-packages/wp-single-view/wp-single-view.html
  13. 12
      frontend/src/app/components/wp-activity/comment-service.ts
  14. 6
      frontend/src/app/components/wp-activity/user/user-activity.component.html
  15. 7
      frontend/src/app/components/wp-activity/user/user-activity.component.ts
  16. 8
      frontend/src/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts
  17. 7
      frontend/src/app/components/wp-card-view/services/wp-card-drag-and-drop.service.ts
  18. 3
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  19. 11
      frontend/src/app/components/wp-copy/wp-copy.controller.ts
  20. 6
      frontend/src/app/components/wp-custom-actions/wp-custom-actions/wp-custom-action.component.ts
  21. 3
      frontend/src/app/components/wp-details/wp-details-toolbar.component.ts
  22. 14
      frontend/src/app/components/wp-edit-form/display-field-renderer.ts
  23. 2
      frontend/src/app/components/wp-edit-form/single-view-edit-context.ts
  24. 371
      frontend/src/app/components/wp-edit-form/work-package-changeset.ts
  25. 13
      frontend/src/app/components/wp-edit-form/work-package-edit-field-handler.ts
  26. 43
      frontend/src/app/components/wp-edit-form/work-package-edit-form.ts
  27. 154
      frontend/src/app/components/wp-edit-form/work-package-editing-service.ts
  28. 44
      frontend/src/app/components/wp-edit-form/work-package-editing.service.interface.ts
  29. 10
      frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts
  30. 9
      frontend/src/app/components/wp-edit-form/work-package-filter-values.ts
  31. 277
      frontend/src/app/components/wp-edit/work-package-changeset.ts
  32. 32
      frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts
  33. 31
      frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts
  34. 20
      frontend/src/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts
  35. 12
      frontend/src/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts
  36. 5
      frontend/src/app/components/wp-fast-table/builders/primary-render-pass.ts
  37. 4
      frontend/src/app/components/wp-fast-table/builders/relations/relations-render-pass.ts
  38. 6
      frontend/src/app/components/wp-fast-table/builders/rows/single-row-builder.ts
  39. 3
      frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
  40. 7
      frontend/src/app/components/wp-fast-table/wp-table-editing.ts
  41. 21
      frontend/src/app/components/wp-inline-create/wp-inline-create.component.ts
  42. 17
      frontend/src/app/components/wp-new/wp-create.controller.ts
  43. 53
      frontend/src/app/components/wp-new/wp-create.service.interface.ts
  44. 79
      frontend/src/app/components/wp-new/wp-create.service.ts
  45. 11
      frontend/src/app/components/wp-table/drag-and-drop/actions/group-by-drag-action.service.ts
  46. 84
      frontend/src/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts
  47. 55
      frontend/src/app/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts
  48. 45
      frontend/src/app/components/wp-table/timeline/cells/wp-timeline-cell-mouse-handler.ts
  49. 3
      frontend/src/app/components/wp-table/timeline/cells/wp-timeline-cell.ts
  50. 10
      frontend/src/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts
  51. 4
      frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts
  52. 8
      frontend/src/app/components/wp-table/timeline/wp-timeline.ts
  53. 9
      frontend/src/app/modules/boards/board/board-list/board-list.component.ts
  54. 56
      frontend/src/app/modules/fields/changeset/changeset.ts
  55. 73
      frontend/src/app/modules/fields/changeset/edit-changeset.ts
  56. 121
      frontend/src/app/modules/fields/changeset/resource-changeset.ts
  57. 8
      frontend/src/app/modules/fields/display/display-field.module.ts
  58. 44
      frontend/src/app/modules/fields/edit/edit-field.component.ts
  59. 14
      frontend/src/app/modules/fields/edit/editing-portal/edit-form-portal.component.ts
  60. 7
      frontend/src/app/modules/fields/edit/editing-portal/edit-form-portal.injector.ts
  61. 2
      frontend/src/app/modules/fields/edit/editing-portal/wp-editing-portal-service.ts
  62. 11
      frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts
  63. 2
      frontend/src/app/modules/grids/grid/area.service.ts
  64. 2
      frontend/src/app/modules/grids/widgets/abstract-widget.component.ts
  65. 4
      frontend/src/app/modules/grids/widgets/custom-text/custom-text-changeset.ts
  66. 7
      frontend/src/app/modules/grids/widgets/custom-text/custom-text-edit-field.service.ts
  67. 2
      frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html
  68. 4
      frontend/src/app/modules/grids/widgets/widget-changeset.ts
  69. 4
      frontend/src/app/modules/hal/resources/work-package-resource.spec.ts
  70. 4
      frontend/src/app/modules/hal/resources/work-package-resource.ts
  71. 15
      frontend/src/app/modules/hal/services/hal-resource.config.ts
  72. 8
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  73. 13
      frontend/src/app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive.ts
  74. 7
      frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts
  75. 8
      frontend/src/app/modules/work_packages/routing/wp-list/wp.list.component.html
  76. 8
      frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts

@ -31,10 +31,6 @@ import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, ViewChild} from "@angular/core";
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {TypeResource} from "core-app/modules/hal/resources/type-resource";
import {CurrentProjectService} from "core-components/projects/current-project.service";
@Component({
templateUrl: './wiki-include-page-macro.modal.html'

@ -31,6 +31,7 @@ import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, ViewChild} from "@angular/core";
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {TypeResource} from "core-app/modules/hal/resources/type-resource";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service";
@ -67,7 +68,7 @@ export class WpButtonMacroModal extends OpModalComponent implements AfterViewIni
constructor(readonly elementRef:ElementRef,
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly currentProject:CurrentProjectService,
protected currentProject:CurrentProjectService,
readonly workPackageDmService:WorkPackageDmService,
readonly cdRef:ChangeDetectorRef,
readonly I18n:I18nService) {

@ -33,10 +33,10 @@ import {Directive, ElementRef, Input, Inject} from "@angular/core";
import {LinkHandling} from "core-app/modules/common/link-handling/link-handling";
import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive";
import {TypeResource} from 'core-app/modules/hal/resources/type-resource';
import {TypeDmService} from "core-app/modules/hal/dm-services/type-dm.service";
import {Highlighting} from 'core-app/components/wp-fast-table/builders/highlighting/highlighting.functions';
import {BrowserDetector} from "core-app/modules/common/browser/browser-detector.service";
import {WorkPackageCreateService} from 'core-components/wp-new/wp-create.service';
import {IWorkPackageCreateServiceToken} from 'core-components/wp-new/wp-create.service.interface';
@Directive({
selector: '[opTypesCreateDropdown]'
@ -51,8 +51,8 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger {
constructor(readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
readonly browserDetector:BrowserDetector,
readonly $state:StateService,
@Inject(IWorkPackageCreateServiceToken) protected wpCreate:WorkPackageCreateService) {
readonly wpCreate:WorkPackageCreateService,
readonly $state:StateService) {
super(elementRef, opContextMenu);
}

@ -32,7 +32,6 @@ import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
import {States} from "core-components/states.service";
import {FormResource} from 'core-app/modules/hal/resources/form-resource';
import {IWorkPackageEditingServiceToken} from "../../wp-edit-form/work-package-editing.service.interface";
@Directive({
selector: '[wpCreateSettingsMenu]'
@ -42,7 +41,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger
constructor(readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
readonly states:States,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService) {
readonly wpEditing:WorkPackageEditingService) {
super(elementRef, opContextMenu);
}
@ -51,8 +50,8 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger
const wp = this.states.workPackages.get('new').value;
if (wp) {
const changeset = this.wpEditing.changesetFor(wp);
changeset.getForm().then(
const change = this.wpEditing.changeFor(wp);
change.getForm().then(
(loadedForm:FormResource) => {
this.buildItems(loadedForm);
this.opContextMenu.show(this, evt);

@ -35,7 +35,6 @@ import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notific
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource';
import {IWorkPackageEditingServiceToken} from "../../wp-edit-form/work-package-editing.service.interface";
import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
@ -51,8 +50,8 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger {
readonly opContextMenu:OPContextMenuService,
readonly $state:StateService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpEditing:WorkPackageEditingService,
protected notificationService:NotificationsService,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService,
protected I18n:I18nService,
protected wpEvents:WorkPackageEventsService) {
@ -60,13 +59,13 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger {
}
protected open(evt:JQuery.TriggeredEvent) {
const changeset = this.wpEditing.changesetFor(this.workPackage);
const change = this.wpEditing.changeFor(this.workPackage);
changeset.getForm().then((form:any) => {
change.getForm().then((form:any) => {
const statuses = form.schema.status.allowedValues;
this.buildItems(statuses);
const writable = changeset.isWritable('status');
const writable = change.schema.status.writable;
if (!writable) {
this.notificationService.addError(this.I18n.t('js.work_packages.message_work_package_status_blocked'));
} else {
@ -83,15 +82,16 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger {
}
private updateStatus(status:HalResource) {
const changeset = this.wpEditing.changesetFor(this.workPackage);
changeset.setValue('status', status);
const change = this.wpEditing.changeFor(this.workPackage);
change.projectedResource.status = status;
if (!this.workPackage.isNew) {
changeset.save().then(() => {
this.wpNotificationsService.showSave(this.workPackage);
this.wpEvents.push({ type: 'updated', id: this.workPackage.id! });
});
this.wpEditing
.save(change)
.then(() => {
this.wpNotificationsService.showSave(this.workPackage);
this.wpEvents.push({ type: 'updated', id: this.workPackage.id! });
});
}
}

@ -39,8 +39,8 @@ import {SchemaCacheService} from 'core-components/schemas/schema-cache.service';
import {States} from 'core-components/states.service';
import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service';
import {IWorkPackageCreateServiceToken} from 'core-components/wp-new/wp-create.service.interface';
import {take, takeWhile} from 'rxjs/operators';
import {WorkPackageCreateService} from '../wp-new/wp-create.service';
import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service";
describe('WorkPackageCacheService', () => {
@ -64,7 +64,7 @@ describe('WorkPackageCacheService', () => {
{provide: PathHelperService, useValue: {}},
{provide: I18nService, useValue: {t: (...args:any[]) => 'translation'}},
{provide: WorkPackageResource, useValue: {}},
{provide: IWorkPackageCreateServiceToken, useValue: {}},
{provide: WorkPackageCreateService, useValue: {}},
{provide: NotificationsService, useValue: {}},
{provide: WorkPackageNotificationService, useValue: {}},
{provide: OpenProjectFileUploadService, useValue: {}}

@ -1,18 +1,18 @@
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";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
export abstract class WorkPackageCommentFieldHandler extends EditFieldHandler implements OnInit {
public fieldName = 'comment';
public handler = this;
public inEdit = false;
public active = false;
public inEditMode = false;
public inFlight = false;
public changeset:WorkPackageChangeset;
public change:WorkPackageChangeset;
// Destroy events
public onDestroy = new Subject<void>();
@ -22,6 +22,10 @@ export abstract class WorkPackageCommentFieldHandler extends EditFieldHandler im
super();
}
public ngOnInit() {
this.change = new WorkPackageChangeset(this.workPackage);
}
/**
* Handle saving the comment
*/
@ -39,16 +43,12 @@ export abstract class WorkPackageCommentFieldHandler extends EditFieldHandler im
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 });
this.change.setValue('comment' , { raw: withText });
}
public get schema():IFieldSchema {
@ -66,24 +66,20 @@ export abstract class WorkPackageCommentFieldHandler extends EditFieldHandler im
}
public get commentValue() {
return this.changeset.value('comment');
return this.change.value('comment');
}
public handleUserCancel() {
this.deactivate(true);
}
public get active() {
return this.inEdit;
}
public activate(withText?:string) {
this.inEdit = true;
this.active = true;
this.reset(withText);
}
deactivate(focus:boolean):void {
this.inEdit = false;
this.active = false;
this.onDestroy.next();
this.onDestroy.complete();
}

@ -9,7 +9,7 @@
<div class="wp-edit-field inplace-edit">
<edit-form-portal *ngIf="active"
[schemaInput]="schema"
[changesetInput]="changeset"
[changeInput]="change"
[editFieldHandler]="handler">
</edit-form-portal>
<div *ngIf="!active"

@ -34,10 +34,12 @@ import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/a
import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service";
import {CommentService} from "core-components/wp-activity/comment-service";
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
Inject, Injector,
Injector,
Input,
OnDestroy,
OnInit,
@ -49,11 +51,11 @@ import {ConfigurationService} from "core-app/modules/common/config/configuration
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
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({
selector: 'work-package-comment',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './work-package-comment.component.html'
})
export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler implements OnInit, OnDestroy {
@ -73,7 +75,6 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler
public inFlight = false;
public canAddComment:boolean;
public showAbove:boolean;
public changeset:WorkPackageChangeset;
constructor(protected elementRef:ElementRef,
protected injector:Injector,
@ -84,6 +85,7 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler
protected wpCacheService:WorkPackageCacheService,
protected wpNotificationsService:WorkPackageNotificationService,
protected NotificationsService:NotificationsService,
protected cdRef:ChangeDetectorRef,
protected I18n:I18nService) {
super(elementRef, injector);
}
@ -131,11 +133,14 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler
if (!this.showAbove) {
this.scrollToBottom();
}
this.cdRef.detectChanges();
}
public deactivate(focus:boolean) {
focus && this.focus();
this.inEdit = false;
this.active = false;
this.cdRef.detectChanges();
}
public async handleUserSubmit() {
@ -148,7 +153,7 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler
let indicator = this.loadingIndicator.wpDetails;
return indicator.promise = this.commentService.createComment(this.workPackage, this.commentValue)
.then(() => {
this.inEdit = false;
this.active = false;
this.NotificationsService.addSuccess(this.I18n.t('js.work_packages.comment_added'));
this.wpLinkedActivities.require(this.workPackage, true);

@ -2,7 +2,6 @@ import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {takeUntil} from 'rxjs/operators';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {combineLatest} from 'rxjs';
@Component({
templateUrl: './wp-relations-count.html',

@ -50,8 +50,9 @@ import {input, InputState} from 'reactivestates';
import {DisplayFieldService} from 'core-app/modules/fields/display/display-field.service';
import {DisplayField} from 'core-app/modules/fields/display/display-field.module';
import {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {IWorkPackageEditingServiceToken} from '../../wp-edit-form/work-package-editing.service.interface';
import {HookService} from 'core-app/modules/plugins/hook-service';
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {Subject} from "rxjs";
import {randomString} from "core-app/helpers/random-string";
import {BrowserDetector} from "core-app/modules/common/browser/browser-detector.service";
import {PortalCleanupService} from "core-app/modules/fields/display/display-portal/portal-cleanup.service";
@ -103,7 +104,7 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
// State updated when structural changes to the single view may occur.
// (e.g., when changing the type or project context).
public resourceContextChange:InputState<ResourceContextChange> = input<ResourceContextChange>();
public resourceContextChange = new Subject<ResourceContextChange>();
// Project context as an indicator
// when editing the work package in a different project
@ -141,7 +142,7 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
protected currentProject:CurrentProjectService,
protected PathHelper:PathHelperService,
protected states:States,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService,
protected wpEditing:WorkPackageEditingService,
protected halResourceService:HalResourceService,
protected displayFieldService:DisplayFieldService,
protected wpCacheService:WorkPackageCacheService,
@ -160,51 +161,58 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
this.workPackage.attachments.updateElements();
}
const change = this.wpEditing.changeFor(this.workPackage);
this.resourceContextChange.next(this.contextFrom(change));
this.refresh(change);
// Whenever the resource context changes in any way,
// update the visible fields.
this.resourceContextChange
.values$()
.pipe(
takeUntil(componentDestroyed(this)),
distinctUntilChanged<ResourceContextChange>((a, b) => _.isEqual(a, b)),
map(() => this.wpEditing.temporaryEditResource(this.workPackage.id!).value!)
map(() => this.wpEditing.changeFor(this.workPackage))
)
.subscribe((resource:WorkPackageResource) => {
// Prepare the fields that are required always
const isNew = this.workPackage.isNew;
if (!resource.project) {
this.projectContext = { matches: false, href: null };
} else {
this.projectContext = {
href: this.PathHelper.projectWorkPackagePath(resource.project.idFromLink, this.workPackage.id!),
matches: resource.project.href === this.currentProject.apiv3Path
};
}
if (isNew && (!this.currentProject.inProjectContext || this.showProject)) {
this.projectContext.field = this.getFields(resource, ['project']);
}
const attributeGroups = resource.schema._attributeGroups;
this.groupedFields = this.rebuildGroupedFields(resource, attributeGroups);
this.cdRef.detectChanges();
});
.subscribe((change:WorkPackageChangeset) => this.refresh(change));
// Update the resource context on every update to the temporary resource.
// This allows detecting a changed type value in a new work package.
this.wpEditing.temporaryEditResource(this.workPackage.id!)
this.wpEditing
.state(this.workPackage.id!)
.values$()
.pipe(
takeUntil(componentDestroyed(this))
)
.subscribe((resource:WorkPackageResource) => {
this.resourceContextChange.putValue(this.contextFrom(resource));
.subscribe((change:WorkPackageChangeset) => {
this.resourceContextChange.next(this.contextFrom(change));
});
}
ngOnDestroy() {
this.cleanupService.clear();
// Nothing to do
}
private refresh(change:WorkPackageChangeset) {
// Prepare the fields that are required always
const isNew = this.workPackage.isNew;
const resource = change.projectedResource;
if (!resource.project) {
this.projectContext = {matches: false, href: null};
} else {
this.projectContext = {
href: this.PathHelper.projectWorkPackagePath(resource.project.idFromLink, this.workPackage.id!),
matches: resource.project.href === this.currentProject.apiv3Path
};
}
if (isNew && !this.currentProject.inProjectContext) {
this.projectContext.field = this.getFields(resource, ['project']);
}
const attributeGroups = resource.schema._attributeGroups;
this.groupedFields = this.rebuildGroupedFields(resource, attributeGroups);
this.cdRef.detectChanges();
}
/**
@ -363,14 +371,15 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
* Used to identify changes in the schema or project that may result in visual changes
* to the single view.
*
* @param {WorkPackageResource} resource
* @param {WorkPackageChangeset} change
* @returns {SchemaContext}
*/
private contextFrom(resource:WorkPackageResource):ResourceContextChange {
let schema = resource.schema;
private contextFrom(change:WorkPackageChangeset):ResourceContextChange {
let schema = change.schema;
let workPackage = change.projectedResource;
let schemaHref:string|null = null;
let projectHref:string|null = resource.project && resource.project.href;
let projectHref:string|null = workPackage.project && workPackage.project.href;
if (schema.baseSchema) {
schemaHref = schema.baseSchema.href;
@ -380,7 +389,7 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
return {
isNew: resource.isNew,
isNew: workPackage.isNew,
schema: schemaHref,
project: projectHref
};

@ -34,7 +34,7 @@
</div>
<div class="attributes-group -project-context __overflowing_element_container __overflowing_project_context"
*ngIf="projectContext.field"
*ngIf="projectContext && projectContext.field"
data-overflowing-identifier=".__overflowing_project_context">
<div>
<p class="wp-project-context--warning" [textContent]="text.project.required"></p>
@ -58,7 +58,7 @@
</div>
</div>
<div class="attributes-group -project-context hide-when-print" *ngIf="!workPackage.isNew && !projectContext.matches">
<div class="attributes-group -project-context hide-when-print" *ngIf="!workPackage.isNew && projectContext && !projectContext.matches">
<div>
<p>
<span [innerHTML]="projectContextText"></span>

@ -48,10 +48,10 @@ export class CommentService {
private NotificationsService:NotificationsService) {
}
public createComment(workPackage:WorkPackageResource, comment:string) {
public createComment(workPackage:WorkPackageResource, comment:{ raw:string }) {
return workPackage.addComment(
{comment: comment},
{'Content-Type': 'application/json; charset=UTF-8'}
{ comment: comment },
{ 'Content-Type': 'application/json; charset=UTF-8' }
)
.catch((error:any) => this.errorAndReject(error, workPackage));
}
@ -60,14 +60,14 @@ export class CommentService {
const options = {
ajax: {
method: 'PATCH',
data: JSON.stringify({comment: comment}),
data: JSON.stringify({ comment: comment }),
contentType: 'application/json; charset=utf-8'
}
};
return activity.update(
{comment: comment},
{'Content-Type': 'application/json; charset=UTF-8'}
{ comment: comment },
{ 'Content-Type': 'application/json; charset=UTF-8' }
).then((activity:HalResource) => {
this.NotificationsService.addSuccess(
this.I18n.t('js.work_packages.comment_updated')

@ -44,15 +44,15 @@
[src]="PathHelper.attachmentDownloadPath(bcfSnapshot.id, bcfSnapshot.file_name)"
class="activity-thumbnail">
<div class="user-comment" >
<div *ngIf="inEdit" class="inplace-edit">
<div *ngIf="active" class="inplace-edit">
<div class="user-comment--form inplace-edit--write-value">
<edit-form-portal [changesetInput]="changeset"
<edit-form-portal [changeInput]="change"
[schemaInput]="schema"
[editFieldHandler]="handler">
</edit-form-portal>
</div>
</div>
<div *ngIf="!inEdit && (isComment || isBcfComment)"
<div *ngIf="!active && (isComment || isBcfComment)"
class="message wiki"
[innerHtml]="postedComment">
</div>

@ -162,7 +162,7 @@ export class UserActivityComponent extends WorkPackageCommentFieldHandler implem
}
public handleUserSubmit() {
if (this.changeset.inFlight || !this.rawComment) {
if (this.inFlight || !this.rawComment) {
return Promise.resolve();
}
return this.updateComment();
@ -173,6 +173,8 @@ export class UserActivityComponent extends WorkPackageCommentFieldHandler implem
}
public async updateComment() {
this.inFlight = true;
await this.onSubmit();
return this.commentService.updateComment(this.activity, this.rawComment || '')
.then((newActivity:HalResource) => {
@ -181,7 +183,8 @@ export class UserActivityComponent extends WorkPackageCommentFieldHandler implem
this.wpLinkedActivities.require(this.workPackage, true);
this.wpCacheService.updateWorkPackage(this.workPackage);
this.deactivate(true);
});
})
.catch(() => this.deactivate(true));
}
public focusEditIcon() {

@ -30,7 +30,6 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r
import {WorkPackageEditingService} from 'core-components/wp-edit-form/work-package-editing-service';
import {ChangeDetectorRef, Component, Inject, Input, OnDestroy, OnInit} from '@angular/core';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {IWorkPackageEditingServiceToken} from "../../wp-edit-form/work-package-editing.service.interface";
import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service";
@ -54,7 +53,8 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy {
constructor(readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService) {
readonly wpCacheService:WorkPackageCacheService,
readonly wpEditing:WorkPackageEditingService) {
}
ngOnInit() {
@ -101,7 +101,7 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy {
return;
}
return this.changeset.value('status');
return this.changeset.projectedResource.status;
}
public get allowed() {
@ -115,6 +115,6 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy {
}
private get changeset() {
return this.wpEditing.changesetFor(this.workPackage);
return this.wpEditing.changeFor(this.workPackage);
}
}

@ -2,8 +2,6 @@ import {Inject, Injectable, Injector} from '@angular/core';
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {WorkPackageViewOrderService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-order.service";
import {States} from "core-components/states.service";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
@ -11,6 +9,7 @@ import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/w
import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service";
import {DragAndDropHelpers} from "core-app/modules/common/drag-and-drop/drag-and-drop.helpers";
import {WorkPackageCardViewComponent} from "core-components/wp-card-view/wp-card-view.component";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
@Injectable()
export class WorkPackageCardDragAndDropService {
@ -28,7 +27,7 @@ export class WorkPackageCardDragAndDropService {
public constructor(readonly states:States,
readonly injector:Injector,
readonly reorderService:WorkPackageViewOrderService,
@Inject(IWorkPackageCreateServiceToken) readonly wpCreate:WorkPackageCreateService,
readonly wpCreate:WorkPackageCreateService,
readonly wpNotifications:WorkPackageNotificationService,
readonly currentProject:CurrentProjectService,
readonly wpInlineCreate:WorkPackageInlineCreateService) {
@ -145,7 +144,7 @@ export class WorkPackageCardDragAndDropService {
this.wpCreate
.createOrContinueWorkPackage(this.currentProject.identifier)
.then((changeset:WorkPackageChangeset) => {
this.activeInlineCreateWp = changeset.resource;
this.activeInlineCreateWp = changeset.projectedResource;
this.workPackages = this.workPackages;
this.cardView.cdRef.detectChanges();
});

@ -19,7 +19,6 @@ import {QueryColumn} from "app/components/wp-query/query-column";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
@ -98,7 +97,7 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit {
readonly injector:Injector,
readonly $state:StateService,
readonly I18n:I18nService,
@Inject(IWorkPackageCreateServiceToken) readonly wpCreate:WorkPackageCreateService,
readonly wpCreate:WorkPackageCreateService,
readonly wpInlineCreate:WorkPackageInlineCreateService,
readonly wpNotifications:WorkPackageNotificationService,
readonly authorisationService:AuthorisationService,

@ -28,12 +28,11 @@
import {take} from 'rxjs/operators';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChangeset} from 'core-components/wp-edit-form/work-package-changeset';
import {WorkPackageCreateController} from 'core-components/wp-new/wp-create.controller';
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {ChangeDetectionStrategy} from "@angular/core";
export class WorkPackageCopyController extends WorkPackageCreateController {
@ -44,7 +43,7 @@ export class WorkPackageCopyController extends WorkPackageCreateController {
public copying = true;
private wpRelations:WorkPackageRelationsService = this.injector.get(WorkPackageRelationsService);
protected wpEditing:WorkPackageEditingService = this.injector.get<WorkPackageEditingService>(IWorkPackageEditingServiceToken);
protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
ngOnInit() {
super.ngOnInit();
@ -79,14 +78,14 @@ export class WorkPackageCopyController extends WorkPackageCreateController {
}
private createCopyFrom(wp:WorkPackageResource) {
let sourceChangeset = this.wpEditing.changesetFor(wp);
let sourceChangeset = this.wpEditing.changeFor(wp);
return this.wpCreate
.copyWorkPackage(sourceChangeset)
.then((copyChangeset) => {
this.__initialized_at = copyChangeset.resource.__initialized_at;
this.__initialized_at = copyChangeset.pristineResource.__initialized_at;
this.wpCacheService.updateWorkPackage(copyChangeset.resource);
this.wpCacheService.updateWorkPackage(copyChangeset.pristineResource);
this.wpEditing.updateValue('new', copyChangeset);
return copyChangeset;

@ -34,7 +34,6 @@ import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notific
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service';
import {CustomActionResource} from 'core-app/modules/hal/resources/custom-action-resource';
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service';
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 {SchemaCacheService} from "core-components/schemas/schema-cache.service";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
@ -53,8 +52,9 @@ export class WpCustomActionComponent {
private wpSchemaCacheService:SchemaCacheService,
private wpActivity:WorkPackagesActivityService,
private wpNotificationsService:WorkPackageNotificationService,
private wpEvents:WorkPackageEventsService,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService) {}
private wpEditing:WorkPackageEditingService,
private wpEvents:WorkPackageEventsService) {
}
private fetchAction() {
this.halResourceService.get<CustomActionResource>(this.action.href!)

@ -29,7 +29,6 @@ import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-se
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {Component, Inject, Input} from '@angular/core';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
@Component({
selector: 'wp-details-toolbar',
@ -43,5 +42,5 @@ export class WorkPackageSplitViewToolbarComponent {
}
constructor(readonly I18n:I18nService,
@Inject(IWorkPackageEditingServiceToken) readonly wpEditing:WorkPackageEditingService) {}
readonly wpEditing:WorkPackageEditingService) {}
}

@ -1,5 +1,4 @@
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChangeset} from './work-package-changeset';
import {Injector} from '@angular/core';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {IFieldSchema} from "core-app/modules/fields/field.base";
@ -7,6 +6,7 @@ import {DisplayFieldContext, DisplayFieldService} from "core-app/modules/fields/
import {DisplayField} from "core-app/modules/fields/display/display-field.module";
import {MultipleLinesStringObjectsDisplayField} from "core-app/modules/fields/display/field-types/wp-display-multiple-lines-string-objects-field.module";
import {ProgressTextDisplayField} from "core-app/modules/fields/display/field-types/wp-display-progress-text-field.module";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {MultipleLinesUserFieldModule} from "core-app/modules/fields/display/field-types/wp-display-multiple-lines-user-field.module";
export const editableClassName = '-editable';
@ -33,10 +33,10 @@ export class DisplayFieldRenderer {
public render(workPackage:WorkPackageResource,
name:string,
changeset:WorkPackageChangeset|null,
change:WorkPackageChangeset|null,
placeholder = cellEmptyPlaceholder):HTMLSpanElement {
const [field, span] = this.renderFieldValue(workPackage, name, changeset, placeholder);
const [field, span] = this.renderFieldValue(workPackage, name, change, placeholder);
if (field === null) {
return span;
@ -49,7 +49,7 @@ export class DisplayFieldRenderer {
public renderFieldValue(workPackage:WorkPackageResource,
name:string,
changeset:WorkPackageChangeset|null,
change:WorkPackageChangeset|null,
placeholder = cellEmptyPlaceholder):[DisplayField|null, HTMLSpanElement] {
const span = document.createElement('span');
const schemaName = workPackage.getSchemaName(name);
@ -61,7 +61,7 @@ export class DisplayFieldRenderer {
return [null, span];
}
const field = this.getField(workPackage, fieldSchema, schemaName, changeset);
const field = this.getField(workPackage, fieldSchema, schemaName, change);
field.render(span, this.getText(field, placeholder));
const title = field.title;
@ -76,7 +76,7 @@ export class DisplayFieldRenderer {
public getField(workPackage:WorkPackageResource,
fieldSchema:IFieldSchema,
name:string,
changeset:WorkPackageChangeset|null):DisplayField {
change:WorkPackageChangeset|null):DisplayField {
let field = this.fieldCache[name];
if (!field) {
@ -84,7 +84,7 @@ export class DisplayFieldRenderer {
}
field.apply(workPackage, fieldSchema);
field.changeset = changeset;
field.activeChange = change;
return field;
}

@ -75,7 +75,7 @@ export class SingleViewEditContext implements WorkPackageEditContext {
public async reset(workPackage:WorkPackageResource, fieldName:string, focus:boolean = false) {
const ctrl = await this.fieldCtrl(fieldName);
ctrl.reset(workPackage);
ctrl.reset();
ctrl.deactivate(focus);
}

@ -1,371 +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 {debugLog} from '../../helpers/debug_output';
import {SchemaCacheService} from '../schemas/schema-cache.service';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {FormResource} from 'core-app/modules/hal/resources/form-resource';
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service';
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service';
import {
IWorkPackageCreateService,
IWorkPackageCreateServiceToken
} from "core-components/wp-new/wp-create.service.interface";
import {
IWorkPackageEditingService,
IWorkPackageEditingServiceToken
} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {EditChangeset} from 'core-app/modules/fields/changeset/edit-changeset';
export class WorkPackageChangeset extends EditChangeset<WorkPackageResource> {
// Injections
public wpNotificationsService:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService);
public schemaCacheService:SchemaCacheService = this.injector.get(SchemaCacheService);
public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService);
public wpCreate:IWorkPackageCreateService = this.injector.get(IWorkPackageCreateServiceToken);
public wpEditing:IWorkPackageEditingService = this.injector.get(IWorkPackageEditingServiceToken);
public wpActivity:WorkPackagesActivityService = this.injector.get(WorkPackagesActivityService);
public halResourceService:HalResourceService = this.injector.get(HalResourceService);
public inFlight:boolean = false;
private wpFormPromise:Promise<FormResource>|null;
public reset(key:string) {
delete this.changes[key];
}
public isChanged(attribute:string) {
return this.changes[attribute];
}
public clear() {
this.changes = {};
this.resetForm();
this.buildResource();
}
/**
* Remove some of the changes by key
* @param changes
*/
public clearSome(...changes:string[]) {
changes.forEach((key) => {
delete this.changes[key];
});
}
private resetForm() {
this.form = null;
}
public setValue(key:string, val:any) {
super.setValue(key, val);
// Update the form for fields that may alter the form itself
// when the work package is new. Otherwise, the save request afterwards
// will update the form automatically.
if (this.resource.isNew && (key === 'project' || key === 'type')) {
this.updateForm();
}
}
public getForm():Promise<FormResource> {
if (!this.form) {
return this.updateForm();
} else {
return Promise.resolve(this.form);
}
}
/**
* Update the form resource from the API.
*/
public updateForm():Promise<FormResource<WorkPackageResource>> {
let payload = this.buildPayloadFromChanges();
if (!this.wpFormPromise) {
this.wpFormPromise = this.resource.$links
.update(payload)
.then((form:FormResource) => {
this.form = form;
this.buildResource();
this.wpFormPromise = null;
return form;
})
.catch((error:any) => {
this.resetForm();
this.wpFormPromise = null;
throw error;
});
}
return this.wpFormPromise;
}
public save():Promise<WorkPackageResource> {
this.inFlight = true;
const wasNew = this.resource.isNew;
let promise = new Promise<WorkPackageResource>((resolve, reject) => {
this.updateForm()
.then((form) => {
const payload = this.buildPayloadFromChanges();
// Reject errors when occurring in form validation
const errors = form.getErrors();
if (errors !== null) {
return reject(errors);
}
this.resource.$links.updateImmediately(payload)
.then((savedWp:WorkPackageResource) => {
// Ensure the schema is loaded before updating
this.schemaCacheService.ensureLoaded(savedWp).then(() => {
// Clear any previous activities
this.wpActivity.clear(this.resource.id!);
// Initialize any potentially new HAL values
savedWp.retainFrom(this.resource);
this.inFlight = false;
this.resource = savedWp;
this.wpCacheService.updateWorkPackage(this.resource, true);
if (wasNew) {
this.resource.overriddenSchema = undefined;
this.wpCreate.newWorkPackageCreated(this.resource);
}
// If there is a parent, its view has to be updated as well
if (this.resource.parent) {
this.wpCacheService.loadWorkPackage(this.resource.parent.id.toString(), true);
}
this.clear();
this.wpEditing.stopEditing(this.resource.id!);
resolve(this.resource);
});
})
.catch((error:any) => {
// Update the resource anyway
this.buildResource();
reject(error);
})
.catch(reject);
});
});
promise
.catch(() => this.inFlight = false);
return promise;
}
/**
* Merge the current changes into the payload resource.
*
* @param {FormResource} form
* @return {any}
*/
private applyChanges(plainPayload:any) {
// Fall back to the last known state of the work package should the form not be loaded.
let reference = this.resource.$source;
if (this.form) {
reference = this.form.payload.$source;
}
_.each(this.changes, (val:any, key:string) => {
const fieldSchema = this.schema[key];
if (!(typeof(fieldSchema) === 'object' && fieldSchema.writable === true)) {
debugLog(`Trying to write ${key} but is not writable in schema`);
return;
}
// Override in _links if it is a linked property
if (reference._links[key]) {
plainPayload._links[key] = this.getLinkedValue(val, fieldSchema);
} else {
plainPayload[key] = this.changes[key];
}
});
return plainPayload;
}
/**
* Create the payload from the current changes, and extend it with the current lock version.
* -- This is the place to add additional logic when the lockVersion changed in between --
*/
private buildPayloadFromChanges() {
let payload;
if (this.resource.isNew) {
// If the work package is new, we need to pass the entire form payload
// to let all default values be transmitted (type, status, etc.)
if (this.form) {
payload = this.form.payload.$source;
} else {
payload = this.resource.$source;
}
// Add attachments to be assigned.
// They will already be created on the server but now
// we need to claim them for the newly created work package.
payload['_links']['attachments'] = this.resource
.attachments
.elements
.map((a:HalResource) => { return { href: a.href }; });
// Explicitly delete the description if it was not set by the user.
// if it was set by the user, #applyChanges will set it again.
// Otherwise, the backend will set it for us.
delete payload.description;
} else {
// Otherwise, simply use the bare minimum, which is the lock version.
payload = this.minimalPayload;
}
return this.applyChanges(payload);
}
private get minimalPayload() {
return {lockVersion: this.resource.lockVersion, _links: {}};
}
/**
* Extract the link(s) in the given changed value
*/
private getLinkedValue(val:any, fieldSchema:IFieldSchema) {
// Links should always be nullified as { href: null }, but
// this wasn't always the case, so ensure null values are returned as such.
if (_.isNil(val)) {
return {href: null};
}
// Test if we either have a CollectionResource or a HAL array,
// or a single hal value.
let isArrayType = (fieldSchema.type || '').startsWith('[]');
let isArray = false;
if (val.forEach || val.elements) {
isArray = true;
}
if (isArray && isArrayType) {
let links:{ href:string }[] = [];
if (val) {
let elements = (val.forEach && val) || val.elements;
elements.forEach((link:{ href:string }) => {
if (link.href) {
links.push({href: link.href});
}
});
}
return links;
} else {
return {href: _.get(val, 'href', null)};
}
}
/**
* Check whether the given attribute is writable.
* @param attribute
*/
public isWritable(attribute:string):boolean {
const schemaName = this.getSchemaName(attribute);
const fieldSchema = this.schema[schemaName] as IFieldSchema;
return fieldSchema && fieldSchema.writable;
}
public humanName(attribute:string):string {
const fieldSchema = this.schema[attribute] as IFieldSchema;
return fieldSchema.name || attribute;
}
public getSchemaName(attribute:string):string {
if (this.schema.hasOwnProperty('date') && (attribute === 'startDate' || attribute === 'dueDate')) {
return 'date';
} else {
return super.getSchemaName(attribute);
}
}
private buildResource() {
let payload = this.sourceFromResourceAndForm();
if (!payload) {
return;
}
const resource = this.halResourceService.createHalResourceOfType('WorkPackage', this.applyChanges(payload));
if (resource.isNew && this.form) {
resource.initializeNewResource(this.form);
}
if (resource.isNew) {
resource.attachments = this.resource.attachments;
}
resource.overriddenSchema = this.schema;
resource.__initialized_at = this.resource.__initialized_at;
this.resource = (resource as WorkPackageResource);
this.wpEditing.updateValue(this.resource.id!, this);
}
/**
* Constructs the source from a combination of the resource
* and the form payload. The payload takes precedences.
* That way, values, that stem from the backend take precedence.
*/
private sourceFromResourceAndForm() {
if (!this.wpCacheService.state(this.resource.id!).value) {
return null;
}
let payload = _.merge({},
this.wpCacheService.state(this.resource.id!).value!.$source);
if (this.form) {
_.merge(payload, this.form.payload.$source);
}
return payload;
}
}

@ -44,7 +44,7 @@ import {PathHelperService} from "core-app/modules/common/path-helper/path-helper
export class WorkPackageEditFieldHandler extends EditFieldHandler {
// Injections
readonly FocusHelper:FocusHelperService = this.injector.get(FocusHelperService)
readonly FocusHelper:FocusHelperService = this.injector.get(FocusHelperService);
readonly ConfigurationService = this.injector.get(ConfigurationService);
readonly I18n:I18nService = this.injector.get(I18nService);
@ -89,7 +89,7 @@ export class WorkPackageEditFieldHandler extends EditFieldHandler {
}
public get inFlight() {
return this.form.changeset.inFlight;
return this.form.change.inFlight;
}
public get context():WorkPackageEditContext {
@ -133,7 +133,7 @@ export class WorkPackageEditFieldHandler extends EditFieldHandler {
* Handle a user submitting the field (e.g, ng-change)
*/
public handleUserSubmit():Promise<any> {
if (this.form.changeset.inFlight || this.form.editMode) {
if (this.inFlight || this.form.editMode) {
return Promise.resolve();
}
@ -180,7 +180,7 @@ export class WorkPackageEditFieldHandler extends EditFieldHandler {
* Cancel any pending changes
*/
public reset() {
this.form.changeset.reset(this.fieldName);
this.form.change.reset(this.fieldName);
this.deactivate(true);
}
@ -205,7 +205,7 @@ export class WorkPackageEditFieldHandler extends EditFieldHandler {
* Returns whether the field has been changed
*/
public isChanged():boolean {
return this.form.changeset.isOverridden(this.fieldName);
return this.form.change.contains(this.fieldName);
}
/**
@ -219,8 +219,7 @@ export class WorkPackageEditFieldHandler extends EditFieldHandler {
* Reference the current set project
*/
public get project() {
const changeset = this.form.changeset;
return changeset.value('project');
return this.form.change.projectedResource.project;
}
/**

@ -33,12 +33,13 @@ import {Subscription} from 'rxjs';
import {States} from '../states.service';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
import {WorkPackageChangeset} from './work-package-changeset';
import {WorkPackageEditContext} from './work-package-edit-context';
import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler';
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
import {FormResource} from "core-app/modules/hal/resources/form-resource";
export const activeFieldContainerClassName = 'wp-inline-edit--active-field';
export const activeFieldClassName = 'wp-inline-edit--field';
@ -47,7 +48,7 @@ export class WorkPackageEditForm {
// Injections
public states:States = this.injector.get(States);
public wpCacheService = this.injector.get(WorkPackageCacheService);
public wpEditing = this.injector.get(IWorkPackageEditingServiceToken);
public wpEditing = this.injector.get(WorkPackageEditingService);
public wpNotificationsService = this.injector.get(WorkPackageNotificationService);
public wpEvents = this.injector.get(WorkPackageEventsService);
@ -94,13 +95,13 @@ export class WorkPackageEditForm {
/**
* Return the current or a new changeset for the given work package.
* This will always return a valid (potentially empty) changeset.
* Return the current or a new change object for the given work package.
* This will always return a valid (potentially empty) change.
*
* @return {WorkPackageChangeset}
*/
public get changeset():WorkPackageChangeset {
return this.wpEditing.changesetFor(this.workPackage);
public get change():WorkPackageChangeset {
return this.wpEditing.changeFor(this.workPackage);
}
/**
@ -139,7 +140,7 @@ export class WorkPackageEditForm {
* Activate all fields that are returned in validation errors
*/
public activateMissingFields() {
this.changeset.getForm().then((form:any) => {
this.change.getForm().then((form:any) => {
_.each(form.validationErrors, (val:any, key:string) => {
if (key === 'id') {
return;
@ -154,9 +155,7 @@ export class WorkPackageEditForm {
* @return {any}
*/
public async submit():Promise<WorkPackageResource> {
const isInitial = this.workPackage.isNew;
if (this.changeset.empty && !isInitial) {
if (this.change.isEmpty() && !this.workPackage.isNew) {
this.closeEditFields();
return Promise.resolve(this.workPackage);
}
@ -171,17 +170,17 @@ export class WorkPackageEditForm {
await Promise.all(_.map(this.activeFields, (handler:WorkPackageEditFieldHandler) => handler.onSubmit()));
return new Promise<WorkPackageResource>((resolve, reject) => {
this.changeset.save()
.then(savedWorkPackage => {
this.wpEditing.save(this.change)
.then(result => {
// Close all current fields
this.closeEditFields(openFields);
resolve(savedWorkPackage);
resolve(result.workPackage);
this.wpNotificationsService.showSave(savedWorkPackage, isInitial);
this.wpNotificationsService.showSave(result.workPackage, result.wasNew);
this.editMode = false;
this.editContext.onSaved(isInitial, savedWorkPackage);
this.wpEvents.push({ type: 'updated', id: savedWorkPackage.id! });
this.editContext.onSaved(result.wasNew, result.workPackage);
this.wpEvents.push({ type: 'updated', id: result.workPackage.id! });
})
.catch((error:ErrorResource|Object) => {
this.wpNotificationsService.handleRawError(error, this.workPackage);
@ -221,7 +220,7 @@ export class WorkPackageEditForm {
fields.forEach((name:string) => {
const handler = this.activeFields[name];
handler && handler.deactivate();
this.changeset.reset(name);
this.change.reset(name);
});
}
@ -275,11 +274,11 @@ export class WorkPackageEditForm {
* @param fieldName
*/
private loadFieldSchema(fieldName:string, noWarnings:boolean = false):Promise<IFieldSchema> {
const schemaName = this.changeset.getSchemaName(fieldName);
const schemaName = this.workPackage.getSchemaName(fieldName);
return new Promise((resolve, reject) => {
this.loadFormAndCheck(schemaName, noWarnings);
const fieldSchema:IFieldSchema = this.changeset.schema[schemaName];
const fieldSchema:IFieldSchema = this.change.schema[schemaName];
if (!fieldSchema) {
throw new Error();
@ -295,10 +294,10 @@ export class WorkPackageEditForm {
* @param noWarnings
*/
private loadFormAndCheck(fieldName:string, noWarnings:boolean = false) {
const schemaName = this.changeset.getSchemaName(fieldName);
const schemaName = this.workPackage.getSchemaName(fieldName);
// Ensure the form is being loaded if necessary
this.changeset
this.change
.getForm()
.then((form) => {
// Look up whether we're actually editable

@ -27,16 +27,19 @@
// ++
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageEditContext} from './work-package-edit-context';
import {WorkPackageChangeset} from './work-package-changeset';
import {combine, deriveRaw, multiInput, MultiInputState, State, StatesGroup} from 'reactivestates';
import {map} from 'rxjs/operators';
import {StateCacheService} from '../states/state-cache.service';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {Injectable, Injector} from '@angular/core';
import {IWorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing.service.interface";
class WPChangesetStates extends StatesGroup {
import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {SchemaCacheService} from "core-components/schemas/schema-cache.service";
import {Subject} from "rxjs";
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {ChangeMap} from "core-app/modules/fields/changeset/changeset";
class WPChangesStates extends StatesGroup {
name = 'WP-Changesets';
changesets = multiInput<WorkPackageChangeset>();
@ -47,40 +50,133 @@ class WPChangesetStates extends StatesGroup {
}
}
/**
* Wrapper class for the saved change of a work package,
* used to access the previous save and or previous state
* of the work package (e.g., whether it was new).
*/
export class WorkPackageChangesetCommit {
/**
* The work package id of the change
* (This is the new work package ID if +wasNew+ is true.
*/
public readonly id:string;
/**
* The resulting, saved work package.
*/
public readonly workPackage:WorkPackageResource;
/** Whether the commit saved an initial work package */
public readonly wasNew:boolean = false;
/** The previous changes */
public readonly changes:ChangeMap;
/**
* Create a change commit from the change object
* @param change The change object that resulted in the save
* @param saved The returned work package
*/
constructor(change:WorkPackageChangeset, saved:WorkPackageResource) {
this.id = saved.id!.toString();
this.wasNew = change.pristineResource.isNew;
this.workPackage = saved;
this.changes = change.changes;
}
}
@Injectable()
export class WorkPackageEditingService extends StateCacheService<WorkPackageChangeset> implements IWorkPackageEditingService {
export class WorkPackageEditingService extends StateCacheService<WorkPackageChangeset> {
/** Committed / saved changes to work packages observable */
public comittedChanges = new Subject<WorkPackageChangesetCommit>();
private stateGroup:WPChangesetStates;
/** State group of changes to wrap */
private stateGroup = new WPChangesStates();
constructor(readonly injector:Injector,
readonly wpActivity:WorkPackagesActivityService,
readonly schemaCache:SchemaCacheService,
readonly wpCacheService:WorkPackageCacheService) {
super();
this.stateGroup = new WPChangesetStates();
}
public async save(change:WorkPackageChangeset):Promise<WorkPackageChangesetCommit> {
change.inFlight = true;
// TODO remove? const wasNew = change.pristineResource.isNew;
// Form the payload we're going to save
const [form, payload] = await change.buildRequestPayload();
// Reject errors when occurring in form validation
const errors = form.getErrors();
if (errors !== null) {
change.inFlight = false;
throw(errors);
}
const savedWp = await change.pristineResource.$links.updateImmediately(payload);
// Ensure the schema is loaded before updating
await this.schemaCache.ensureLoaded(savedWp);
// Initialize any potentially new HAL values
savedWp.retainFrom(change.pristineResource);
this.onSaved(savedWp);
change.inFlight = false;
// Complete the change
return this.complete(change, savedWp);
}
/**
* Mark the given change as completed, notify changes
* and reset it.
*/
private complete(change:WorkPackageChangeset, saved:WorkPackageResource):WorkPackageChangesetCommit {
const commit = new WorkPackageChangesetCommit(change, saved);
this.comittedChanges.next(commit);
this.reset(change);
return commit;
}
/**
* Reset the given change, either due to cancelling or successful submission.
* @param change
*/
public reset(change:WorkPackageChangeset) {
change.clear();
this.clearSome(change.workPackageId);
}
/**
* Start or continue editing the work package with a given edit context
* @param {string} workPackageId
* @param {WorkPackageEditContext} editContext
* @param {boolean} editAll
* @return {WorkPackageChangeset} changeset or null if the associated work package id does not exist
* @param {workPackage} Work package to edit
* @param {form:FormResource} Initialize with an existing form
* @return {WorkPackageChangeset} Change object to work on
*/
public changesetFor(oldReference:WorkPackageResource):WorkPackageChangeset {
const wpId = oldReference.id!;
const workPackage = this.wpCacheService.state(wpId).getValueOr(oldReference);
const state = this.multiState.get(wpId);
public changeFor(fallback:WorkPackageResource, form?:FormResource):WorkPackageChangeset {
const state = this.multiState.get(fallback.id!);
const workPackage = this.wpCacheService.state(fallback.id!).getValueOr(fallback);
let changeset = state.value;
// If there is no changeset, or
// If there is an empty one for a older work package reference
// build a new changeset
if (!changeset || (changeset.empty && changeset.resource.lockVersion < workPackage.lockVersion)) {
changeset = new WorkPackageChangeset(this.injector, workPackage)
if (!changeset || (changeset.isEmpty() && changeset.pristineResource.lockVersion < workPackage.lockVersion)) {
changeset = new WorkPackageChangeset(workPackage, state, form)
state.putValue(changeset);
return changeset;
}
return changeset;
const change = state.value!;
return change;
}
/**
@ -101,9 +197,9 @@ export class WorkPackageEditingService extends StateCacheService<WorkPackageChan
return deriveRaw(combined,
($) => $
.pipe(
map(([wp, changeset]) => {
if (wp && changeset && !changeset.empty) {
return changeset.resource;
map(([wp, change]) => {
if (wp && change && !change.isEmpty()) {
return change.projectedResource;
} else {
return wp;
}
@ -116,13 +212,23 @@ export class WorkPackageEditingService extends StateCacheService<WorkPackageChan
this.multiState.get(workPackageId).clear();
}
protected load(id:string) {
protected load(id:string):Promise<WorkPackageChangeset> {
return this.wpCacheService.require(id)
.then((wp:WorkPackageResource) => {
return new WorkPackageChangeset(this.injector, wp);
return this.changeFor(wp);
});
}
protected onSaved(saved:WorkPackageResource) {
this.wpActivity.clear(saved.id);
// If there is a parent, its view has to be updated as well
if (saved.parent) {
this.wpCacheService.loadWorkPackage(saved.parent.id.toString(), true);
}
this.wpCacheService.updateWorkPackage(saved);
}
protected loadAll(ids:string[]) {
return Promise.all(ids.map(id => this.load(id))) as any;
}

@ -1,44 +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 {InjectionToken} 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";
export const IWorkPackageEditingServiceToken = new InjectionToken<IWorkPackageEditingService>('IWorkPackageEditingService');
/**
* Export an interface for changeset and form
* to avoid circular dependency warnigns due to TS imports.
*/
export interface IWorkPackageEditingService {
updateValue(id:string, changeset:any):void;
stopEditing(id:string):void;
changesetFor(wp:WorkPackageResource):WorkPackageChangeset;
}

@ -32,11 +32,8 @@ import {HalResourceService} from "core-app/modules/hal/services/hal-resource.ser
import {Injector} from "@angular/core";
import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service";
import {SchemaCacheService} from "core-components/schemas/schema-cache.service";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
@ -56,6 +53,7 @@ import {HookService} from "core-app/modules/plugins/hook-service";
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
import {TimezoneService} from "core-components/datetime/timezone.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
describe('WorkPackageFilterValues', () => {
let resource:WorkPackageResource;
@ -92,8 +90,8 @@ describe('WorkPackageFilterValues', () => {
WorkPackageNotificationService,
SchemaCacheService,
WorkPackageCacheService,
{ provide: IWorkPackageCreateServiceToken, useClass: WorkPackageCreateService },
{ provide: IWorkPackageEditingServiceToken, useClass: WorkPackageEditingService },
WorkPackageCreateService,
WorkPackageEditingService,
WorkPackagesActivityService,
]
}).compileComponents();
@ -102,7 +100,7 @@ describe('WorkPackageFilterValues', () => {
halResourceService = injector.get(HalResourceService);
resource = halResourceService.createHalResourceOfClass(WorkPackageResource, source, true);
changeset = new WorkPackageChangeset(injector, resource);
changeset = new WorkPackageChangeset(resource);
let type1 = halResourceService.createHalResourceOfClass(
TypeResource,

@ -1,10 +1,10 @@
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {WorkPackageChangeset} from './work-package-changeset';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
import {CurrentUserService} from "core-components/user/current-user.service";
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service';
import {Injector} from '@angular/core';
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import compareByHrefOrString = AngularTrackingHelpers.compareByHrefOrString;
export class WorkPackageFilterValues {
@ -13,7 +13,7 @@ export class WorkPackageFilterValues {
private halResourceService:HalResourceService = this.injector.get(HalResourceService);
constructor(private injector:Injector,
private changeset:WorkPackageChangeset,
private change:WorkPackageChangeset,
private filters:QueryFilterInstanceResource[],
private excluded:string[] = []) {
@ -51,8 +51,7 @@ export class WorkPackageFilterValues {
let newValue = this.findSpecialValue(value, field) || value;
if (newValue) {
this.changeset.setValue(field, newValue);
this.changeset.resource[field] = newValue;
this.change.projectedResource[field] = newValue;
}
}
@ -85,7 +84,7 @@ export class WorkPackageFilterValues {
return false;
}
const current = this.changeset.value(filter.id);
const current = this.change.projectedResource[filter.id];
for (let i = 0; i < filter.values.length; i++) {
if (compareByHrefOrString(current, filter.values[i])) {

@ -0,0 +1,277 @@
/**
* Temporary class living while a work package is being edited
* Maintains references to:
* - The source work package (a pristine base)
* - The open set of changes (a changeset object)
* - The current form (due to temporary type/project changes)
*
* Provides access to:
* - A projected work package resource with all changes applied
*/
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {debugLog} from "core-app/helpers/debug_output";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {InputState} from "reactivestates";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
export class WorkPackageChangeset extends ResourceChangeset<WorkPackageResource> {
/** Reference and load promise for the current form */
private wpFormPromise:Promise<FormResource>|null;
/** Flag whether this is currently being saved */
public inFlight = false;
constructor(public pristineResource:WorkPackageResource,
public readonly state?:InputState<WorkPackageChangeset>,
form?:FormResource) {
super(pristineResource, form);
}
/**
* Push the change to the editing state to notify others.
* This will happen internally on work-package wide changes
*
* (type, project changes)
*/
public push() {
if (this.state) {
this.state.putValue(this);
}
}
/**
* Retrieve the editing value for the given attribute
*
* @param {string} key The attribute to read
* @return {any} Either the value from the overriden change, or the default value
*/
public value(key:string) {
// Overridden value by user?
if (this.changeset.contains(key)) {
return this.changeset.get(key);
}
// TODO we might need values from the form (default values on type change?)
// Default value from the form?
// const payloadValue = _.get(this._form, ['payload', key]);
// if (payloadValue !== undefined) {
// return payloadValue;
// }
// Return whatever is on the base.
return this.pristineResource[key];
}
public setValue(key:string, val:any) {
this.changeset.set(key, val);
// Update the form for fields that may alter the form itself
// when the work package is new. Otherwise, the save request afterwards
// will update the form automatically.
if (this.pristineResource.isNew && (key === 'project' || key === 'type')) {
this.updateForm().then(() => this.push());
}
}
/**
* Revert all edits on the resource
*/
public clear() {
super.clear();
this.state && this.state.clear();
}
/**
* Returns the work package being edited
*/
public get workPackageId():string {
return this.pristineResource.id!.toString();
}
/**
* Return whether the element is writable
* given the current best schema.
*
* @param key
*/
public isWritable(key:string) {
const fieldSchema = this.schema[key] as IFieldSchema|null;
return fieldSchema && fieldSchema.writable;
}
/**
* Return the best humanized name for this attribute
* @param attribute
*/
public humanName(attribute:string):string {
return _.get(this.schema, `${attribute}.name`, attribute);
}
/**
* Build the request attributes against the fresh form
*/
public buildRequestPayload():Promise<[FormResource, Object]> {
return this
.updateForm()
.then(form => [form, this.buildPayloadFromChanges()]) as Promise<[FormResource, Object]>;
}
/**
* Returns the current work package form.
* This may be different from the base form when project or type is changed.
*/
public getForm():Promise<FormResource> {
if (!this.form) {
return this.updateForm();
} else {
return Promise.resolve(this.form);
}
}
public getSchemaName(attribute:string):string {
return this.projectedResource.getSchemaName(attribute);
}
/**
* Update the form resource from the API.
*/
private updateForm():Promise<FormResource> {
let payload = this.buildPayloadFromChanges();
if (!this.wpFormPromise) {
this.wpFormPromise = this.pristineResource.$links
.update(payload)
.then((form:FormResource) => {
this.wpFormPromise = null;
this.form = form;
this.push();
return form;
})
.catch((error:any) => {
this.wpFormPromise = null;
this.form = null;
throw error;
}) as Promise<FormResource>;
}
return this.wpFormPromise;
}
/**
* Create the payload from the current changes, and extend it with the current lock version.
* -- This is the place to add additional logic when the lockVersion changed in between --
*/
private buildPayloadFromChanges() {
let payload;
if (this.pristineResource.isNew) {
// If the work package is new, we need to pass the entire form payload
// to let all default values be transmitted (type, status, etc.)
if (this.form) {
payload = this.form.payload.$source;
} else {
payload = this.pristineResource.$source;
}
// Add attachments to be assigned.
// They will already be created on the server but now
// we need to claim them for the newly created work package.
payload['_links']['attachments'] = this.pristineResource
.attachments
.elements
.map((a:HalResource) => {
return { href: a.href };
});
// Explicitly delete the description if it was not set by the user.
// if it was set by the user, #applyChanges will set it again.
// Otherwise, the backend will set it for us.
delete payload.description;
} else {
// Otherwise, simply use the bare minimum, which is the lock version.
payload = this.minimalPayload;
}
return this.applyChanges(payload);
}
private get minimalPayload() {
return { lockVersion: this.pristineResource.lockVersion, _links: {} };
}
/**
* Merge the current changes into the payload resource.
*
* @param {plainPayload:unknown} A set of attributes to merge into the payload
* @return {any}
*/
private applyChanges(plainPayload:any) {
// Fall back to the last known state of the work package should the form not be loaded.
let reference = this.pristineResource.$source;
if (this.form) {
reference = this.form.payload.$source;
}
_.each(this.changeset.all, (val:unknown, key:string) => {
const fieldSchema:IFieldSchema|undefined = this.schema[key];
if (!(typeof (fieldSchema) === 'object' && fieldSchema.writable)) {
debugLog(`Trying to write ${key} but is not writable in schema`);
return;
}
// Override in _links if it is a linked property
if (reference._links[key]) {
plainPayload._links[key] = this.getLinkedValue(val, fieldSchema);
} else {
plainPayload[key] = val;
}
});
return plainPayload;
}
/**
* Extract the link(s) in the given changed value
*/
private getLinkedValue(val:any, fieldSchema:IFieldSchema) {
// Links should always be nullified as { href: null }, but
// this wasn't always the case, so ensure null values are returned as such.
if (_.isNil(val)) {
return { href: null };
}
// Test if we either have a CollectionResource or a HAL array,
// or a single hal value.
let isArrayType = (fieldSchema.type || '').startsWith('[]');
let isArray = false;
if (val.forEach || val.elements) {
isArray = true;
}
if (isArray && isArrayType) {
let links:{ href:string }[] = [];
if (val) {
let elements = (val.forEach && val) || val.elements;
elements.forEach((link:{ href:string }) => {
if (link.href) {
links.push({ href: link.href });
}
});
}
return links;
} else {
return { href: _.get(val, 'href', null) };
}
}
}

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Component, Inject, Injector, Input, OnDestroy, OnInit} from '@angular/core';
import {Component, Injector, Input, OnDestroy, OnInit} from '@angular/core';
import {StateService, Transition, TransitionService} from '@uirouter/core';
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {WorkPackageEditFieldComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field.component';
@ -42,8 +42,6 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r
import {WorkPackageNotificationService} from '../wp-notification.service';
import {WorkPackageCreateService} from './../../wp-new/wp-create.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {IWorkPackageEditingServiceToken} from "../../wp-edit-form/work-package-editing.service.interface";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
@Component({
@ -62,8 +60,8 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
constructor(protected states:States,
protected injector:Injector,
@Inject(IWorkPackageCreateServiceToken) protected wpCreate:WorkPackageCreateService,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService,
protected wpCreate:WorkPackageCreateService,
protected wpEditing:WorkPackageEditingService,
protected wpNotificationsService:WorkPackageNotificationService,
protected wpTableSelection:WorkPackageViewSelectionService,
protected wpTableFocus:WorkPackageViewFocusService,
@ -106,14 +104,6 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
ngOnInit() {
const context = new SingleViewEditContext(this.injector, this);
this.form = WorkPackageEditForm.createInContext(this.injector, context, this.workPackage, this.initializeEditMode);
this.states.workPackages.get(this.workPackage.id!)
.values$()
.pipe(
takeUntil(componentDestroyed(this)),
)
.subscribe((wp) => {
_.each(this.fields, (ctrl) => this.updateDisplayField(ctrl, wp));
});
if (this.initializeEditMode) {
this.start();
@ -145,15 +135,10 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
this.registeredFields.putValue(_.keys(this.fields));
const shouldActivate =
(this.editMode && !this.skipField(field) || this.form.activeFields[field.fieldName])
(this.editMode && !this.skipField(field) || this.form.activeFields[field.fieldName]);
if (shouldActivate) {
field.activateOnForm(true);
} else {
this.states.workPackages
.get(this.workPackage.id!)
.valuesPromise()
.then(wp => this.updateDisplayField(field, wp!));
}
}
@ -203,11 +188,6 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
}
}
private updateDisplayField(field:WorkPackageEditFieldComponent, wp:WorkPackageResource) {
field.workPackage = wp;
field.render();
}
private skipField(field:WorkPackageEditFieldComponent) {
const fieldName = field.fieldName;
@ -219,8 +199,8 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
}
// Only skip if value present and not changed in changeset
const hasDefault = this.workPackage[fieldName]
const changed = this.form.changeset.isChanged(fieldName);
const hasDefault = this.workPackage[fieldName];
const changed = this.form.change.changes[fieldName];
return hasDefault && !changed;
}

@ -55,7 +55,7 @@ import {NotificationsService} from 'core-app/modules/common/notifications/notifi
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position";
import {IWorkPackageEditingServiceToken} from "../../wp-edit-form/work-package-editing.service.interface";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
@Component({
selector: 'wp-edit-field',
@ -87,7 +87,7 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy {
protected wpNotificationsService:WorkPackageNotificationService,
protected ConfigurationService:ConfigurationService,
protected opContextMenu:OPContextMenuService,
@Inject(IWorkPackageEditingServiceToken) protected wpEditing:WorkPackageEditingService,
protected wpEditing:WorkPackageEditingService,
protected wpCacheService:WorkPackageCacheService,
// Get parent field group from injector
protected wpEditFieldGroup:WorkPackageEditFieldGroupComponent,
@ -108,6 +108,17 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy {
this.fieldRenderer = new DisplayFieldRenderer(this.injector, 'single-view', this.displayFieldOptions);
this.$element = jQuery(this.elementRef.nativeElement);
this.wpEditFieldGroup.register(this);
this.wpEditing
.temporaryEditResource(this.workPackageId)
.values$()
.pipe(
untilComponentDestroyed(this)
)
.subscribe(workPackage => {
this.workPackage = workPackage;
this.render();
});
}
public ngOnDestroy() {
@ -126,7 +137,7 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy {
}
public render() {
const el = this.fieldRenderer.render(this.resource, this.fieldName, null, this.displayPlaceholder);
const el = this.fieldRenderer.render(this.workPackage, this.fieldName, null, this.displayPlaceholder);
this.displayContainer.nativeElement.innerHTML = '';
this.displayContainer.nativeElement.appendChild(el);
}
@ -141,15 +152,9 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy {
}
}
public get resource() {
return this.wpEditing
.temporaryEditResource(this.workPackageId)
.getValueOr(this.workPackage);
}
public get isEditable() {
const fieldSchema = this.resource.schema[this.fieldName] as IFieldSchema;
return this.resource.isAttributeEditable(this.fieldName) && fieldSchema && fieldSchema.writable;
const fieldSchema = this.workPackage.schema[this.fieldName] as IFieldSchema;
return this.workPackage.isAttributeEditable(this.fieldName) && fieldSchema && fieldSchema.writable;
}
public activateIfEditable(event:JQuery.TriggeredEvent) {
@ -212,10 +217,8 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy {
return false;
}
public reset(workPackage:WorkPackageResource) {
this.workPackage = workPackage;
public reset() {
this.render();
this.deactivate();
}

@ -1,15 +1,18 @@
import {Injector} from '@angular/core';
import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {States} from '../../../../states.service';
import {ancestorClassIdentifier, hierarchyGroupClass} from '../../../helpers/wp-table-hierarchy-helpers';
import {WorkPackageTable} from '../../../wp-fast-table';
import {WorkPackageTableRow} from '../../../wp-table.interfaces';
import {PrimaryRenderPass, RowRenderInfo} from '../../primary-render-pass';
import {additionalHierarchyRowClassName, SingleHierarchyRowBuilder} from './single-hierarchy-row-builder';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageViewHierarchiesService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy.service";
import {PrimaryRenderPass, RowRenderInfo} from "core-components/wp-fast-table/builders/primary-render-pass";
import {States} from "core-components/states.service";
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {WorkPackageTableRow} from "core-components/wp-fast-table/wp-table.interfaces";
import {
ancestorClassIdentifier,
hierarchyGroupClass
} from "core-components/wp-fast-table/helpers/wp-table-hierarchy-helpers";
import {WorkPackageViewHierarchies} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-table-hierarchies";
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {WorkPackageViewHierarchiesService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy.service";
export class HierarchyRenderPass extends PrimaryRenderPass {
@ -172,8 +175,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
// Iterate ancestors
ancestors.forEach((el:WorkPackageResource, index:number) => {
const ancestor = this.states.workPackages.get(el.id!).value!;
const ancestor = this.states.workPackages.get(el.id!).getValueOr(el);
// If we see the parent the first time,
// build it as an additional row and insert it into the ancestry

@ -1,13 +1,13 @@
import {Injector} from '@angular/core';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {States} from '../../../../states.service';
import {SingleRowBuilder} from "core-components/wp-fast-table/builders/rows/single-row-builder";
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {States} from "core-components/states.service";
import {
collapsedGroupClass,
hierarchyGroupClass,
hierarchyRootClass
} from '../../../helpers/wp-table-hierarchy-helpers';
import {WorkPackageTable} from '../../../wp-fast-table';
import {SingleRowBuilder} from '../../rows/single-row-builder';
} from "core-components/wp-fast-table/helpers/wp-table-hierarchy-helpers";
import {WorkPackageViewHierarchiesService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy.service";
export const indicatorCollapsedClass = '-hierarchy-collapsed';
@ -113,8 +113,8 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {
/**
* Append to the row of hierarchy level <level> a hierarchy indicator.
* @param workPackage
* @param row
* @param level
* @param jRow jQuery row element
* @param level Indentation level
*/
private appendHierarchyIndicator(workPackage:WorkPackageResource, jRow:JQuery, level?:number):void {
const hierarchyLevel = level === undefined || null ? workPackage.ancestors.length : level;

@ -8,9 +8,6 @@ import {WorkPackageTable} from '../wp-fast-table';
import {RelationRenderInfo, RelationsRenderPass} from './relations/relations-render-pass';
import {SingleRowBuilder} from './rows/single-row-builder';
import {TimelineRenderPass} from './timeline/timeline-render-pass';
import {
IWorkPackageEditingServiceToken
} from "../../wp-edit-form/work-package-editing.service.interface";
import {HighlightingRenderPass} from "core-components/wp-fast-table/builders/highlighting/row-highlight-render-pass";
import {DragDropHandleRenderPass} from "core-components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-render-pass";
import {RenderedWorkPackage} from "core-app/modules/work_packages/render-info/rendered-work-package.type";
@ -39,7 +36,7 @@ export interface RowRenderInfo {
export abstract class PrimaryRenderPass {
protected readonly wpEditing:WorkPackageEditingService = this.injector.get<WorkPackageEditingService>(IWorkPackageEditingServiceToken);
protected readonly wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
protected readonly states:States = this.injector.get(States);
protected readonly I18n:I18nService = this.injector.get(I18nService);

@ -1,11 +1,11 @@
import {Injector} from '@angular/core';
import {RelationResource} from 'core-app/modules/hal/resources/relation-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageRelationsService} from '../../../wp-relations/wp-relations.service';
import {WorkPackageTable} from '../../wp-fast-table';
import {PrimaryRenderPass, RowRenderInfo} from '../primary-render-pass';
import {relationGroupClass, RelationRowBuilder} from './relation-row-builder';
import {QueryColumn} from 'core-components/wp-query/query-column';
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {WorkPackageViewColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service";
import {
RelationColumnType,

@ -175,14 +175,14 @@ export class SingleRowBuilder {
}
protected buildEmptyRow(workPackage:WorkPackageResource, row:HTMLTableRowElement):[HTMLTableRowElement, boolean] {
const changeset = this.workPackageTable.editing.changeset(workPackage.id!);
const change = this.workPackageTable.editing.change(workPackage.id!);
let cells:{ [attribute:string]:JQuery } = {};
if (changeset && !changeset.empty) {
if (change && !change.isEmpty()) {
// Try to find an old instance of this row
const oldRow = locateTableRowByIdentifier(this.classIdentifier(workPackage));
changeset.changedAttributes.forEach((attribute:string) => {
change.changedAttributes.forEach((attribute:string) => {
cells[attribute] = oldRow.find(`.${wpCellTdClassName}.${attribute}`);
});
}

@ -8,13 +8,12 @@ import {WorkPackageTable} from '../../wp-fast-table';
import {ClickOrEnterHandler} from '../click-or-enter-handler';
import {TableEventHandler} from '../table-handler-registry';
import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position";
import {IWorkPackageEditingServiceToken} from "../../../wp-edit-form/work-package-editing.service.interface";
export class EditCellHandler extends ClickOrEnterHandler implements TableEventHandler {
// Injections
public states:States = this.injector.get(States);
public wpEditing:WorkPackageEditingService = this.injector.get<WorkPackageEditingService>(IWorkPackageEditingServiceToken);
public wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
// Keep a reference to all

@ -1,15 +1,14 @@
import {Injector} from '@angular/core';
import {WorkPackageChangeset} from 'core-components/wp-edit-form/work-package-changeset';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {TableRowEditContext} from '../wp-edit-form/table-row-edit-context';
import {WorkPackageEditForm} from '../wp-edit-form/work-package-edit-form';
import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-service';
import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table';
import {IWorkPackageEditingServiceToken} from "../wp-edit-form/work-package-editing.service.interface";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
export class WorkPackageTableEditingContext {
public wpEditing:WorkPackageEditingService = this.injector.get<WorkPackageEditingService>(IWorkPackageEditingServiceToken);
public wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
constructor(readonly table:WorkPackageTable,
readonly injector:Injector) {
@ -22,7 +21,7 @@ export class WorkPackageTableEditingContext {
this.forms = {};
}
public changeset(workPackageId:string):WorkPackageChangeset | undefined {
public change(workPackageId:string):WorkPackageChangeset | undefined {
return this.wpEditing.state(workPackageId).value;
}

@ -27,14 +27,13 @@
// ++
import {
AfterViewInit, ChangeDetectorRef,
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
Inject,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit
} from '@angular/core';
@ -42,7 +41,6 @@ import {AuthorisationService} from 'core-app/modules/common/model-auth/model-aut
import {WorkPackageViewFocusService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service';
import {filter} from 'rxjs/operators';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChangeset} from '../wp-edit-form/work-package-changeset';
import {WorkPackageEditForm} from '../wp-edit-form/work-package-edit-form';
import {onClickOrEnter} from '../wp-fast-table/handlers/click-or-enter-handler';
import {WorkPackageTable} from '../wp-fast-table/wp-fast-table';
@ -56,15 +54,10 @@ import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/iso
import {componentDestroyed, untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper';
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {CurrentUserService} from "core-components/user/current-user.service";
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {Subscription} from 'rxjs';
import {WorkPackageViewColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service";
import {
WorkPackageEvent,
WorkPackageEventsService
} from "core-app/modules/work_packages/events/work-package-events.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
@Component({
selector: '[wpInlineCreate]',
@ -100,7 +93,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, AfterViewInit,
protected readonly I18n:I18nService,
protected readonly querySpace:IsolatedQuerySpace,
protected readonly cdRef:ChangeDetectorRef,
@Inject(IWorkPackageCreateServiceToken) protected readonly wpCreate:WorkPackageCreateService,
protected readonly wpCreate:WorkPackageCreateService,
protected readonly wpInlineCreate:WorkPackageInlineCreateService,
protected readonly wpTableColumns:WorkPackageViewColumnsService,
protected readonly wpTableFocus:WorkPackageViewFocusService,
@ -223,15 +216,15 @@ export class WorkPackageInlineCreateComponent implements OnInit, AfterViewInit,
public addWorkPackageRow() {
this.wpCreate
.createOrContinueWorkPackage(this.projectIdentifier)
.then((changeset:WorkPackageChangeset) => {
.then((change:WorkPackageChangeset) => {
const wp = this.currentWorkPackage = changeset.resource;
const wp = this.currentWorkPackage = change.projectedResource;
this.editingSubscription = this
.wpCreate
.changesetUpdates$()
.pipe(
filter((cs) => !!this.currentWorkPackage && !!cs.form),
filter(() => !!this.currentWorkPackage),
).subscribe((form) => {
if (!this.isActive) {
this.insertRow(wp);

@ -42,16 +42,15 @@ import {States} from '../states.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {RootResource} from 'core-app/modules/hal/resources/root-resource';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {WorkPackageChangeset} from '../wp-edit-form/work-package-changeset';
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
import {WorkPackageCreateService} from './wp-create.service';
import {takeUntil} from 'rxjs/operators';
import {RootDmService} from 'core-app/modules/hal/dm-services/root-dm.service';
import {OpTitleService} from 'core-components/html/op-title.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {CurrentUserService} from "core-app/components/user/current-user.service";
import {WorkPackageViewFiltersService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-filters.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
@Injectable()
@ -59,7 +58,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
public successState:string;
public newWorkPackage:WorkPackageResource;
public parentWorkPackage:WorkPackageResource;
public changeset:WorkPackageChangeset;
public change:WorkPackageChangeset;
/** Are we in the copying substates ? */
public copying = false;
@ -77,7 +76,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
readonly currentUser:CurrentUserService,
protected wpNotificationsService:WorkPackageNotificationService,
protected states:States,
@Inject(IWorkPackageCreateServiceToken) protected wpCreate:WorkPackageCreateService,
protected wpCreate:WorkPackageCreateService,
protected wpTableFilters:WorkPackageViewFiltersService,
protected wpCacheService:WorkPackageCacheService,
protected pathHelper:PathHelperService,
@ -90,17 +89,15 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
this
.createdWorkPackage()
.then((changeset:WorkPackageChangeset) => {
this.changeset = changeset;
this.newWorkPackage = changeset.resource;
this.change = changeset;
this.newWorkPackage = changeset.projectedResource;
this.cdRef.detectChanges();
this.setTitle();
if (this.stateParams['parent_id']) {
this.changeset.setValue(
'parent',
{ href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path }
);
this.newWorkPackage.parent =
{ href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path };
}
// Load the parent simply to display the type name :-/

@ -1,53 +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 {InjectionToken} from "@angular/core";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {Observable} from "rxjs";
export const IWorkPackageCreateServiceToken = new InjectionToken<IWorkPackageCreateService>('IWorkPackageCreateService');
/**
* Export an interface for changeset and form
* to avoid circular dependency warnigns due to TS imports.
*/
export interface IWorkPackageCreateService {
/**
* Observable for new work packages as created through various sources
* on the WP frontend.
*/
onNewWorkPackage():Observable<WorkPackageResource>;
/**
* Notifier callback for new work packages
* @param wp
*/
newWorkPackageCreated(wp:WorkPackageResource):void;
}

@ -26,25 +26,24 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Inject, Injectable, Injector} from '@angular/core';
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {Injectable, Injector, OnDestroy} from '@angular/core';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {Observable, Subject} from 'rxjs';
import {WorkPackageChangeset} from '../wp-edit-form/work-package-changeset';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service';
import {IWorkPackageCreateService} from "core-components/wp-new/wp-create.service.interface";
import {HookService} from 'core-app/modules/plugins/hook-service';
import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values";
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 {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {filter} from "rxjs/operators";
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service";
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
@Injectable()
export class WorkPackageCreateService implements IWorkPackageCreateService {
export class WorkPackageCreateService implements OnDestroy {
protected form:Promise<FormResource>|undefined;
// Allow callbacks to happen on newly created work packages
@ -54,13 +53,25 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
protected hooks:HookService,
protected wpCacheService:WorkPackageCacheService,
protected halResourceService:HalResourceService,
@Inject(IWorkPackageEditingServiceToken) protected readonly wpEditing:WorkPackageEditingService,
protected readonly querySpace:IsolatedQuerySpace,
protected wpEditing:WorkPackageEditingService,
protected workPackageDmService:WorkPackageDmService,
protected readonly wpEvents:WorkPackageEventsService) {
this.wpEditing
.comittedChanges
.pipe(
untilComponentDestroyed(this),
filter(commit => commit.wasNew)
)
.subscribe(commit => this.newWorkPackageCreated(commit.workPackage));
}
ngOnDestroy() {
// Nothing to do
}
public newWorkPackageCreated(wp:WorkPackageResource) {
protected newWorkPackageCreated(wp:WorkPackageResource) {
this.form = undefined;
this.wpEvents.push({ type: 'created', id: wp.id! });
this.newWorkPackageCreatedSubject.next(wp);
@ -86,16 +97,16 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
let wp = this.halResourceService.createHalResourceOfType<WorkPackageResource>('WorkPackage', form.payload.$plain());
wp.initializeNewResource(form);
const changeset = new WorkPackageChangeset(this.injector, wp, form);
const change = this.wpEditing.changeFor(wp, form);
// Call work package initialization hook
this.hooks.call('workPackageNewInitialization', changeset);
this.hooks.call('workPackageNewInitialization', change);
return changeset;
return change;
}
public copyWorkPackage(copyFrom:WorkPackageChangeset) {
let request = copyFrom.resource.$source;
let request = copyFrom.pristineResource.$source;
// Ideally we would make an empty request before to get the create schema (cannot use the update schema of the source changeset)
// to get all the writable attributes and only send those.
@ -117,7 +128,7 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
wp.initializeNewResource(form);
return new WorkPackageChangeset(this.injector, wp, form);
return this.wpEditing.changeFor(wp, form);
}
@ -143,31 +154,31 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
}
public createOrContinueWorkPackage(projectIdentifier:string|null|undefined, type?:number) {
let changesetPromise = this.continueExistingEdit(type);
let changePromise = this.continueExistingEdit(type);
if (!changesetPromise) {
changesetPromise = this.createNewWithDefaults(projectIdentifier, type);
if (!changePromise) {
changePromise = this.createNewWithDefaults(projectIdentifier, type);
}
return changesetPromise.then((changeset) => {
this.wpEditing.updateValue('new', changeset);
this.wpCacheService.updateWorkPackage(changeset.resource);
return changePromise.then((change) => {
this.wpEditing.updateValue('new', change);
this.wpCacheService.updateWorkPackage(change.pristineResource);
return changeset;
return change;
});
}
protected continueExistingEdit(type?:number) {
const changeset = this.wpEditing.state('new').value;
if (changeset !== undefined) {
const changeType = changeset.resource.type;
const change = this.wpEditing.state('new').value;
if (change !== undefined) {
const changeType = change.projectedResource.type;
const hasChanges = !changeset.empty;
const hasChanges = !change.isEmpty();
const typeEmpty = !changeType && !type;
const typeMatches = type && changeType && changeType.idFromLink === type.toString();
if (hasChanges && (typeEmpty || typeMatches)) {
return Promise.resolve(changeset);
return Promise.resolve(change);
}
}
@ -175,16 +186,16 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
}
protected createNewWithDefaults(projectIdentifier:string|null|undefined, type?:number) {
let changesetPromise = null;
let changePromise = null;
if (type) {
changesetPromise = this.createNewTypedWorkPackage(projectIdentifier, type);
changePromise = this.createNewTypedWorkPackage(projectIdentifier, type);
} else {
changesetPromise = this.createNewWorkPackage(projectIdentifier);
changePromise = this.createNewWorkPackage(projectIdentifier);
}
return changesetPromise.then((changeset:WorkPackageChangeset) => {
if (!changeset) {
return changePromise.then((change:WorkPackageChangeset) => {
if (!change) {
throw 'No new work package was created';
}
@ -194,9 +205,9 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
except = ['type'];
}
this.applyDefaults(changeset, changeset.resource, except);
this.applyDefaults(change, change.projectedResource, except);
return changeset;
return change;
});
}
@ -207,13 +218,13 @@ export class WorkPackageCreateService implements IWorkPackageCreateService {
* @param wp
* @param except
*/
private applyDefaults(changeset:WorkPackageChangeset, wp:WorkPackageResource, except:string[]) {
private applyDefaults(change:WorkPackageChangeset, wp:WorkPackageResource, except:string[]) {
// Not using WorkPackageViewFiltersService here as the embedded table does not load the form
// which will result in that service having empty current filters.
let query = this.querySpace.query.value;
if (query) {
const filter = new WorkPackageFilterValues(this.injector, changeset, query.filters, except);
const filter = new WorkPackageFilterValues(this.injector, change, query.filters, except);
filter.applyDefaultsFromFilters();
}
}

@ -5,13 +5,12 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa
import {rowGroupClassName} from "core-components/wp-fast-table/builders/modes/grouped/grouped-classes.constants";
import {locatePredecessorBySelector} from "core-components/wp-fast-table/helpers/wp-table-row-helpers";
import {groupIdentifier} from "core-components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers";
import {IWorkPackageEditingServiceToken} from "core-components/wp-edit-form/work-package-editing.service.interface";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
export class GroupByDragActionService extends TableDragActionService {
private wpTableGroupBy = this.injector.get(WorkPackageViewGroupByService);
private wpEditing = this.injector.get<WorkPackageEditingService>(IWorkPackageEditingServiceToken);
private wpEditing = this.injector.get<WorkPackageEditingService>(WorkPackageEditingService);
private wpNotifications = this.injector.get(WorkPackageNotificationService);
public get applies() {
@ -27,12 +26,12 @@ export class GroupByDragActionService extends TableDragActionService {
}
public handleDrop(workPackage:WorkPackageResource, el:HTMLElement):Promise<unknown> {
const changeset = this.wpEditing.changesetFor(workPackage);
const changeset = this.wpEditing.changeFor(workPackage);
const groupedValue = this.getValueForGroup(el);
changeset.setValue(this.groupedAttribute!, groupedValue);
return changeset
.save()
changeset.projectedResource[this.groupedAttribute!] = groupedValue;
return this.wpEditing
.save(changeset)
.catch(e => this.wpNotifications.handleRawError(e, workPackage));
}

@ -22,7 +22,6 @@ import {
} from './wp-timeline-cell';
import {classNameBarLabel, classNameLeftHandle, classNameRightHandle} from './wp-timeline-cell-mouse-handler';
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
import {WorkPackageChangeset} from '../../../wp-edit-form/work-package-changeset';
import {DisplayFieldRenderer} from '../../../wp-edit-form/display-field-renderer';
import {Injector} from '@angular/core';
import {TimezoneService} from 'core-components/datetime/timezone.service';
@ -30,6 +29,7 @@ import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/
import {HierarchyRenderPass} from "core-components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass";
import Moment = moment.Moment;
import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
export interface CellDateMovement {
// Target values to move work package to
@ -88,32 +88,32 @@ export class TimelineCellRenderer {
* For generic work packages, assigns start and finish date.
*
*/
public assignDateValues(changeset:WorkPackageChangeset,
public assignDateValues(change:WorkPackageChangeset,
labels:WorkPackageCellLabels,
dates:any):void {
this.assignDate(changeset, 'startDate', dates.startDate);
this.assignDate(changeset, 'dueDate', dates.dueDate);
this.assignDate(change, 'startDate', dates.startDate);
this.assignDate(change, 'dueDate', dates.dueDate);
this.updateLabels(true, labels, changeset);
this.updateLabels(true, labels, change);
}
/**
* Handle movement by <delta> days of the work package to either (or both) edge(s)
* depending on which initial date was set.
*/
public onDaysMoved(changeset:WorkPackageChangeset,
public onDaysMoved(change:WorkPackageChangeset,
dayUnderCursor:Moment,
delta:number,
direction:'left' | 'right' | 'both' | 'create' | 'dragright'):CellDateMovement {
const initialStartDate = changeset.resource.startDate;
const initialDueDate = changeset.resource.dueDate;
const initialStartDate = change.pristineResource.startDate;
const initialDueDate = change.pristineResource.dueDate;
const now = moment().format('YYYY-MM-DD');
const startDate = moment(changeset.value('startDate'));
const dueDate = moment(changeset.value('dueDate'));
const startDate = moment(change.projectedResource.startdate);
const dueDate = moment(change.projectedResource.dueDate);
let dates:CellDateMovement = {};
@ -157,7 +157,7 @@ export class TimelineCellRenderer {
return 'both'; // irrelevant
}
const changeset = renderInfo.changeset;
const projection = renderInfo.change.projectedResource;
let direction:'left' | 'right' | 'both' | 'dragright';
// Update the cursor and maybe set start/due values
@ -165,8 +165,8 @@ export class TimelineCellRenderer {
// only left
direction = 'left';
this.workPackageTimeline.forceCursor('col-resize');
if (changeset.value('startDate') === null) {
changeset.setValue('startDate', changeset.value('dueDate'));
if (projection.startDate === null) {
projection.startDate = projection['dueDate'];
}
} else if (jQuery(ev.target!).hasClass(classNameRightHandle) || dateForCreate) {
// only right
@ -179,18 +179,18 @@ export class TimelineCellRenderer {
}
if (dateForCreate) {
changeset.setValue('startDate', dateForCreate);
changeset.setValue('dueDate', dateForCreate);
projection.startDate = dateForCreate;
projection.dueDate = dateForCreate;
direction = 'dragright';
}
this.updateLabels(true, labels, renderInfo.changeset);
this.updateLabels(true, labels, renderInfo.change);
return direction;
}
public onMouseDownEnd(labels:WorkPackageCellLabels, changeset:WorkPackageChangeset) {
this.updateLabels(false, labels, changeset);
public onMouseDownEnd(labels:WorkPackageCellLabels, change:WorkPackageChangeset) {
this.updateLabels(false, labels, change);
}
/**
@ -198,12 +198,12 @@ export class TimelineCellRenderer {
* false, if the element must be removed from the timeline.
*/
public update(element:HTMLDivElement, labels:WorkPackageCellLabels|null, renderInfo:RenderInfo):boolean {
const changeset = renderInfo.changeset;
const change = renderInfo.change;
const bar = element.querySelector(`.${timelineBackgroundElementClass}`) as HTMLElement;
const viewParams = renderInfo.viewParams;
let start = moment(changeset.value('startDate'));
let due = moment(changeset.value('dueDate'));
let start = moment(change.projectedResource.startDate);
let due = moment(change.projectedResource.dueDate);
if (_.isNaN(start.valueOf()) && _.isNaN(due.valueOf())) {
element.style.visibility = 'hidden';
@ -240,7 +240,7 @@ export class TimelineCellRenderer {
// Update labels if any
if (labels) {
this.updateLabels(false, labels, changeset);
this.updateLabels(false, labels, change);
}
this.checkForActiveSelectionMode(renderInfo, bar);
@ -262,10 +262,10 @@ export class TimelineCellRenderer {
}
getMarginLeftOfLeftSide(renderInfo:RenderInfo):number {
const changeset = renderInfo.changeset;
const projection = renderInfo.change.projectedResource;
let start = moment(changeset.value('startDate'));
let due = moment(changeset.value('dueDate'));
let start = moment(projection.startDate);
let due = moment(projection.dueDate);
start = _.isNaN(start.valueOf()) ? due.clone() : start;
const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, 'days');
@ -274,10 +274,10 @@ export class TimelineCellRenderer {
}
getMarginLeftOfRightSide(renderInfo:RenderInfo):number {
const changeset = renderInfo.changeset;
const projection = renderInfo.change.projectedResource;
let start = moment(changeset.value('startDate'));
let due = moment(changeset.value('dueDate'));
let start = moment(projection.startDate);
let due = moment(projection.dueDate);
start = _.isNaN(start.valueOf()) ? due.clone() : start;
due = _.isNaN(due.valueOf()) ? start.clone() : due;
@ -355,7 +355,7 @@ export class TimelineCellRenderer {
element.appendChild(labelHoverRight);
const labels = new WorkPackageCellLabels(labelCenter, labelLeft, labelHoverLeft, labelRight, labelHoverRight, labelFarRight);
this.updateLabels(false, labels, renderInfo.changeset);
this.updateLabels(false, labels, renderInfo.change);
return labels;
}
@ -374,9 +374,9 @@ export class TimelineCellRenderer {
}
}
protected assignDate(changeset:WorkPackageChangeset, attributeName:string, value:moment.Moment) {
protected assignDate(change:WorkPackageChangeset, attributeName:string, value:moment.Moment) {
if (value) {
changeset.setValue(attributeName, value.format('YYYY-MM-DD'));
change.projectedResource[attributeName] = value.format('YYYY-MM-DD');
}
}
@ -409,27 +409,27 @@ export class TimelineCellRenderer {
protected updateLabels(activeDragNDrop:boolean,
labels:WorkPackageCellLabels,
changeset:WorkPackageChangeset) {
change:WorkPackageChangeset) {
const labelConfiguration = this.wpTableTimeline.getNormalizedLabels(changeset.resource);
const labelConfiguration = this.wpTableTimeline.getNormalizedLabels(change.projectedResource);
if (!activeDragNDrop) {
// normal display
this.renderLabel(changeset, labels, 'left', labelConfiguration.left);
this.renderLabel(changeset, labels, 'right', labelConfiguration.right);
this.renderLabel(changeset, labels, 'farRight', labelConfiguration.farRight);
this.renderLabel(change, labels, 'left', labelConfiguration.left);
this.renderLabel(change, labels, 'right', labelConfiguration.right);
this.renderLabel(change, labels, 'farRight', labelConfiguration.farRight);
}
// Update hover labels
this.renderHoverLabels(labels, changeset);
this.renderHoverLabels(labels, change);
}
protected renderHoverLabels(labels:WorkPackageCellLabels, changeset:WorkPackageChangeset) {
this.renderLabel(changeset, labels, 'leftHover', 'startDate');
this.renderLabel(changeset, labels, 'rightHover', 'dueDate');
protected renderHoverLabels(labels:WorkPackageCellLabels, change:WorkPackageChangeset) {
this.renderLabel(change, labels, 'leftHover', 'startDate');
this.renderLabel(change, labels, 'rightHover', 'dueDate');
}
protected renderLabel(changeset:WorkPackageChangeset,
protected renderLabel(change:WorkPackageChangeset,
labels:WorkPackageCellLabels,
position:LabelPosition|'leftHover'|'rightHover',
attribute:string|null) {
@ -450,7 +450,7 @@ export class TimelineCellRenderer {
}
// Get the rendered field
let [field, span] = this.fieldRenderer.renderFieldValue(changeset.resource, attribute, changeset);
let [field, span] = this.fieldRenderer.renderFieldValue(change.projectedResource, attribute, change);
if (label && field && span) {
span.classList.add('label-content');

@ -17,8 +17,8 @@ import {
classNameShowOnHover,
WorkPackageCellLabels
} from './wp-timeline-cell';
import {WorkPackageChangeset} from '../../../wp-edit-form/work-package-changeset';
import Moment = moment.Moment;
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
public get type():string {
@ -27,8 +27,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
public isEmpty(wp:WorkPackageResource) {
const date = moment(wp.date as any);
const noDateValue = _.isNaN(date.valueOf());
return noDateValue;
return _.isNaN(date.valueOf());
}
public canMoveDates(wp:WorkPackageResource) {
@ -62,23 +61,23 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
* For generic work packages, assigns start and finish date .
*
*/
public assignDateValues(changeset:WorkPackageChangeset,
public assignDateValues(change:WorkPackageChangeset,
labels:WorkPackageCellLabels,
dates:any):void {
this.assignDate(changeset, 'date', dates.date);
this.updateLabels(true, labels, changeset);
this.assignDate(change, 'date', dates.date);
this.updateLabels(true, labels, change);
}
/**
* Handle movement by <delta> days of milestone.
*/
public onDaysMoved(changeset:WorkPackageChangeset,
public onDaysMoved(change:WorkPackageChangeset,
dayUnderCursor:Moment,
delta:number,
direction:'left' | 'right' | 'both' | 'create' | 'dragright') {
const initialDate = changeset.resource.date;
const initialDate = change.projectedResource.date;
let dates:CellDateMovement = {};
if (initialDate) {
@ -105,19 +104,19 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
this.workPackageTimeline.forceCursor('ew-resize');
if (dateForCreate) {
renderInfo.changeset.setValue('date', dateForCreate);
renderInfo.change.projectedResource.date = dateForCreate;
direction = 'create';
return direction;
}
this.updateLabels(true, labels, renderInfo.changeset);
this.updateLabels(true, labels, renderInfo.change);
return direction;
}
public update(element:HTMLDivElement, labels:WorkPackageCellLabels|null, renderInfo:RenderInfo): boolean {
public update(element:HTMLDivElement, labels:WorkPackageCellLabels|null, renderInfo:RenderInfo):boolean {
const viewParams = renderInfo.viewParams;
const date = moment(renderInfo.changeset.value('date'));
const date = moment(renderInfo.change.projectedResource.date);
// abort if no date
if (_.isNaN(date.valueOf())) {
@ -139,7 +138,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
// Update labels if any
if (labels) {
this.updateLabels(false, labels, renderInfo.changeset);
this.updateLabels(false, labels, renderInfo.change);
}
this.checkForActiveSelectionMode(renderInfo, diamond);
@ -148,8 +147,8 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
}
getMarginLeftOfLeftSide(renderInfo:RenderInfo):number {
const changeset = renderInfo.changeset;
let start = moment(changeset.value('date'));
const change = renderInfo.change;
let start = moment(change.projectedResource.date);
const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, 'days');
return calculatePositionValueForDayCountingPx(renderInfo.viewParams, offsetStart);
}
@ -208,41 +207,41 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
element.appendChild(labelHoverRight);
const labels = new WorkPackageCellLabels(null, labelLeft, null, labelRight, labelHoverRight, labelFarRight);
this.updateLabels(false, labels, renderInfo.changeset);
this.updateLabels(false, labels, renderInfo.change);
return labels;
}
protected renderHoverLabels(labels:WorkPackageCellLabels, changeset:WorkPackageChangeset) {
this.renderLabel(changeset, labels, 'rightHover', 'date');
protected renderHoverLabels(labels:WorkPackageCellLabels, change:WorkPackageChangeset) {
this.renderLabel(change, labels, 'rightHover', 'date');
}
protected updateLabels(activeDragNDrop:boolean,
labels:WorkPackageCellLabels,
changeset:WorkPackageChangeset) {
change:WorkPackageChangeset) {
const labelConfiguration = this.wpTableTimeline.getNormalizedLabels(changeset.resource);
const labelConfiguration = this.wpTableTimeline.getNormalizedLabels(change.projectedResource);
if (!activeDragNDrop) {
// normal display
// Show only one date field if left=start, right=dueDate
if (labelConfiguration.left === 'startDate' && labelConfiguration.right === 'dueDate') {
this.renderLabel(changeset, labels, 'left', null);
this.renderLabel(changeset, labels, 'right', 'date');
this.renderLabel(change, labels, 'left', null);
this.renderLabel(change, labels, 'right', 'date');
} else {
this.renderLabel(changeset, labels, 'left', labelConfiguration.left);
this.renderLabel(changeset, labels, 'right', labelConfiguration.right);
this.renderLabel(change, labels, 'left', labelConfiguration.left);
this.renderLabel(change, labels, 'right', labelConfiguration.right);
}
this.renderLabel(changeset, labels, 'farRight', labelConfiguration.farRight);
this.renderLabel(change, labels, 'farRight', labelConfiguration.farRight);
}
// Update hover labels
this.renderHoverLabels(labels, changeset);
this.renderHoverLabels(labels, change);
}
protected renderLabel(changeset:WorkPackageChangeset,
protected renderLabel(change:WorkPackageChangeset,
labels:WorkPackageCellLabels,
position:LabelPosition|'leftHover'|'rightHover',
attribute:string|null) {
@ -251,7 +250,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
attribute = 'date';
}
super.renderLabel(changeset, labels, position, attribute);
super.renderLabel(change, labels, position, attribute);
}
}

@ -28,9 +28,7 @@
import {Injector} from '@angular/core';
import * as moment from 'moment';
import {States} from '../../../states.service';
import {WorkPackageCacheService} from '../../../work-packages/work-package-cache.service';
import {WorkPackageChangeset} from '../../../wp-edit-form/work-package-changeset';
import {WorkPackageNotificationService} from '../../../wp-edit/wp-notification.service';
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
import {RenderInfo} from '../wp-timeline';
@ -38,10 +36,12 @@ import {TimelineCellRenderer} from './timeline-cell-renderer';
import {WorkPackageCellLabels} from './wp-timeline-cell';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {QueryDmService} from 'core-app/modules/hal/dm-services/query-dm.service';
import Moment = moment.Moment;
import {keyCodes} from 'core-app/modules/common/keyCodes.enum';
import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service";
import {WorkPackageEditingService} from 'core-app/components/wp-edit-form/work-package-editing-service';
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
import Moment = moment.Moment;
export const classNameBar = 'bar';
export const classNameLeftHandle = 'leftHandle';
@ -54,6 +54,7 @@ export function registerWorkPackageMouseHandler(this:void,
getRenderInfo:() => RenderInfo,
workPackageTimeline:WorkPackageTimelineTableController,
wpCacheService:WorkPackageCacheService,
wpEditing:WorkPackageEditingService,
wpEvents:WorkPackageEventsService,
wpNotificationsService:WorkPackageNotificationService,
loadingIndicator:LoadingIndicatorService,
@ -65,8 +66,8 @@ export function registerWorkPackageMouseHandler(this:void,
const querySpace:IsolatedQuerySpace = injector.get(IsolatedQuerySpace);
let mouseDownStartDay:number|null = null; // also flag to signal active drag'n'drop
renderInfo.changeset = new WorkPackageChangeset(injector, renderInfo.workPackage);
let mouseDownStartDay:number | null = null; // also flag to signal active drag'n'drop
renderInfo.change = wpEditing.changeFor(renderInfo.workPackage);
let dateStates:any;
let placeholderForEmptyCell:HTMLElement;
@ -85,7 +86,7 @@ export function registerWorkPackageMouseHandler(this:void,
function applyDateValues(renderInfo:RenderInfo, dates:{ [name:string]:Moment }) {
// Let the renderer decide which fields we change
renderer.assignDateValues(renderInfo.changeset, labels, dates);
renderer.assignDateValues(renderInfo.change, labels, dates);
}
function getCursorOffsetInDaysFromLeft(renderInfo:RenderInfo, ev:MouseEvent) {
@ -127,7 +128,7 @@ export function registerWorkPackageMouseHandler(this:void,
const offsetDayCurrent = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay);
const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayCurrent, 'days');
dateStates = renderer.onDaysMoved(renderInfo.changeset, dayUnderCursor, days, direction);
dateStates = renderer.onDaysMoved(renderInfo.change, dayUnderCursor, days, direction);
applyDateValues(renderInfo, dateStates);
renderer.update(bar, labels, renderInfo);
};
@ -185,8 +186,8 @@ export function registerWorkPackageMouseHandler(this:void,
const offsetDayCurrent = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay);
const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayCurrent, 'days');
const widthInDays = offsetDayCurrent - offsetDayStart;
const moved = renderer.onDaysMoved(renderInfo.changeset, dayUnderCursor, widthInDays, mouseDownType);
renderer.assignDateValues(renderInfo.changeset, labels, moved);
const moved = renderer.onDaysMoved(renderInfo.change, dayUnderCursor, widthInDays, mouseDownType);
renderer.assignDateValues(renderInfo.change, labels, moved);
renderer.update(bar, labels, renderInfo);
};
@ -218,41 +219,41 @@ export function registerWorkPackageMouseHandler(this:void,
dateStates = {};
// const renderInfo = getRenderInfo();
if (cancelled || renderInfo.changeset.empty) {
renderInfo.changeset.clear();
if (cancelled || renderInfo.change.isEmpty()) {
renderInfo.change.clear();
renderer.update(bar, labels, renderInfo);
renderer.onMouseDownEnd(labels, renderInfo.changeset);
renderer.onMouseDownEnd(labels, renderInfo.change);
workPackageTimeline.refreshView();
} else {
const stopAndRefresh = () => {
renderInfo.changeset.clear();
renderer.onMouseDownEnd(labels, renderInfo.changeset);
renderInfo.change.clear();
renderer.onMouseDownEnd(labels, renderInfo.change);
workPackageTimeline.refreshView();
};
// Persist the changes
saveWorkPackage(renderInfo.changeset)
saveWorkPackage(renderInfo.change)
.then(stopAndRefresh)
.catch(stopAndRefresh);
}
}
function saveWorkPackage(changeset:WorkPackageChangeset) {
function saveWorkPackage(change:WorkPackageChangeset) {
const queryDm:QueryDmService = injector.get(QueryDmService);
const states:States = injector.get(States);
// Remmeber the time before saving the work package to know which work packages to update
// Remember the time before saving the work package to know which work packages to update
const updatedAt = moment().toISOString();
return loadingIndicator.table.promise = changeset.save()
.then((wp) => {
wpNotificationsService.showSave(wp);
return loadingIndicator.table.promise = wpEditing.save(change)
.then((result) => {
wpNotificationsService.showSave(result.workPackage);
const ids = _.map(querySpace.rendered.value!, row => row.workPackageId);
loadingIndicator.table.promise =
queryDm.loadIdsUpdatedSince(ids, updatedAt).then(workPackageCollection => {
wpCacheService.updateWorkPackageList(workPackageCollection.elements);
wpEvents.push({ type: 'updated', id: wp.id! });
wpEvents.push({ type: 'updated', id: result.workPackage.id! });
});
})
.catch((error) => {

@ -36,6 +36,7 @@ import {TimelineMilestoneCellRenderer} from './timeline-milestone-cell-renderer'
import {registerWorkPackageMouseHandler} from './wp-timeline-cell-mouse-handler';
import {Injector} from '@angular/core';
import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service";
import {WorkPackageEditingService} from 'core-app/components/wp-edit-form/work-package-editing-service';
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
export const classNameLeftLabel = 'labelLeft';
@ -62,6 +63,7 @@ export class WorkPackageCellLabels {
export class WorkPackageTimelineCell {
readonly wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService);
readonly wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
readonly wpEvents:WorkPackageEventsService = this.injector.get(WorkPackageEventsService);
readonly wpNotificationsService:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService);
readonly states:States = this.injector.get(States);
@ -159,6 +161,7 @@ export class WorkPackageTimelineCell {
() => this.latestRenderInfo,
this.workPackageTimeline,
this.wpCacheService,
this.wpEditing,
this.wpEvents,
this.wpNotificationsService,
this.loadingIndicator,

@ -28,24 +28,26 @@
import {Injector} from '@angular/core';
import {States} from '../../../states.service';
import {WorkPackageChangeset} from '../../../wp-edit-form/work-package-changeset';
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
import {RenderInfo} from '../wp-timeline';
import {TimelineCellRenderer} from './timeline-cell-renderer';
import {TimelineMilestoneCellRenderer} from './timeline-milestone-cell-renderer';
import {WorkPackageTimelineCell} from './wp-timeline-cell';
import {WorkPackageEditingService} from 'core-app/components/wp-edit-form/work-package-editing-service';
import {RenderedWorkPackage} from "core-app/modules/work_packages/render-info/rendered-work-package.type";
export class WorkPackageTimelineCellsRenderer {
// Injections
public states = this.injector.get(States);
public wpEditing = this.injector.get(WorkPackageEditingService);
public cells:{ [classIdentifier:string]:WorkPackageTimelineCell } = {};
private cellRenderers:{ milestone:TimelineMilestoneCellRenderer, generic:TimelineCellRenderer };
constructor(public readonly injector:Injector, private wpTimeline:WorkPackageTimelineTableController) {
constructor(readonly injector:Injector,
readonly wpTimeline:WorkPackageTimelineTableController) {
this.cellRenderers = {
milestone: new TimelineMilestoneCellRenderer(this.injector, wpTimeline),
generic: new TimelineCellRenderer(this.injector, wpTimeline)
@ -140,7 +142,7 @@ export class WorkPackageTimelineCellsRenderer {
return {
viewParams: this.wpTimeline.viewParameters,
workPackage: wp,
changeset: new WorkPackageChangeset(this.injector, wp)
} as RenderInfo;
change: this.wpEditing.changeFor(wp)
};
}
}

@ -122,7 +122,7 @@ export class WorkPackageTimelineTableController implements AfterViewInit, OnDest
this.wpTableDirective.registerTimeline(this, this.timelineBody[0]);
// Refresh on changes to work packages
this.updateOnWorkPackageChanges();
this.updateOnWorkPackageChangesets();
// Refresh timeline rendering callback
this.setupRefreshListener();
@ -242,7 +242,7 @@ export class WorkPackageTimelineTableController implements AfterViewInit, OnDest
});
}
updateOnWorkPackageChanges() {
updateOnWorkPackageChangesets() {
this.states.workPackages.observeChange()
.pipe(
takeUntil(componentDestroyed(this)),

@ -1,6 +1,4 @@
import {TimelineZoomLevel} from 'core-app/modules/hal/resources/query-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageChangeset} from 'core-components/wp-edit-form/work-package-changeset';
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
@ -30,8 +28,10 @@ import {WorkPackageChangeset} from 'core-components/wp-edit-form/work-package-ch
// ++
import * as moment from 'moment';
import {InputState, MultiInputState} from 'reactivestates';
import Moment = moment.Moment;
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {RenderedWorkPackage} from "core-app/modules/work_packages/render-info/rendered-work-package.type";
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import Moment = moment.Moment;
export const timelineElementCssClass = 'timeline-element';
export const timelineBackgroundElementClass = 'timeline-element--bg';
@ -119,7 +119,7 @@ export class TimelineViewParameters {
export interface RenderInfo {
viewParams:TimelineViewParameters;
workPackage:WorkPackageResource;
changeset:WorkPackageChangeset;
change:WorkPackageChangeset;
}
/**

@ -36,7 +36,6 @@ import {ApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder";
import {BoardService} from "app/modules/boards/board/board.service";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values";
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 {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service";
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
@ -132,7 +131,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
private readonly authorisationService:AuthorisationService,
private readonly wpInlineCreate:WorkPackageInlineCreateService,
protected readonly injector:Injector,
@Inject(IWorkPackageEditingServiceToken) private readonly wpEditing:WorkPackageEditingService,
private readonly wpEditing:WorkPackageEditingService,
private readonly loadingIndicator:LoadingIndicatorService,
private readonly wpCacheService:WorkPackageCacheService,
private readonly boardService:BoardService,
@ -316,7 +315,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
*/
private addWorkPackage(workPackage:WorkPackageResource) {
let query = this.querySpace.query.value!;
const changeset = this.wpEditing.changesetFor(workPackage);
const changeset = this.wpEditing.changeFor(workPackage);
// Ensure attribute remains writable in the form
const actionAttribute = this.board.actionAttribute;
@ -330,12 +329,12 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
const filter = new WorkPackageFilterValues(this.injector, changeset, query.filters);
filter.applyDefaultsFromFilters();
if (changeset.empty) {
if (changeset.isEmpty()) {
// Ensure work package and its schema is loaded
return this.wpCacheService.updateWorkPackage(workPackage);
} else {
// Save changes to the work package, which reloads it as well
return changeset.save();
return this.wpEditing.save(changeset);
}
}

@ -0,0 +1,56 @@
export type ChangeMap = { [attribute:string]:unknown };
export class Changeset {
private changes:ChangeMap = {};
/**
* Return whether a change value exist for the given attribute key.
* @param {string} key
* @return {boolean}
*/
public contains(key:string) {
return this.changes.hasOwnProperty(key);
}
/**
* Get changed attribute names
* @returns {string[]}
*/
public get changed():string[] {
return _.keys(this.changes);
}
/**
* Returns the live set of the changes.
*/
public get all():ChangeMap {
return this.changes;
}
/**
* Reset one or multiple changes
* @param key
*/
public reset(...keys:string[]) {
keys.forEach((k) => delete this.changes[k]);
}
/**
* Reset the entire changeset
*/
public clear():void {
this.changes = {};
}
public set(key:string, value:unknown):void {
this.changes[key] = value;
}
/**
* Get a single value from the changeset
* @param key
*/
public get(key:string):unknown|undefined {
return this.changes[key];
}
}

@ -1,73 +0,0 @@
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource";
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {Injector} from '@angular/core';
export abstract class EditChangeset<T extends HalResource|{ [key:string]:unknown; }> {
// The changeset to be applied to the resource
public changes:{ [attribute:string]:any } = {};
public form:FormResource|null;
constructor(readonly injector:Injector,
public resource:T,
form?:FormResource) {
this.form = form || null;
}
public get empty() {
return _.isEmpty(this.changes);
}
/**
* Get attributes
* @returns {string[]}
*/
public get changedAttributes() {
return _.keys(this.changes);
}
/**
* Retrieve the editing value for the given attribute
*
* @param {string} key The attribute to read
* @return {any} Either the value from the overriden change, or the default value
*/
public value(key:string) {
if (this.isOverridden(key)) {
return this.changes[key];
} else {
return this.resource[key];
}
}
public setValue(key:string, val:any) {
this.changes[key] = val;
}
public getSchemaName(attribute:string):string {
return attribute;
}
public clear() {
this.changes = {};
}
/**
* Return whether a change value exist for the given attribute key.
* @param {string} key
* @return {boolean}
*/
public isOverridden(key:string) {
return this.changes.hasOwnProperty(key);
}
/**
* Get the best schema currently available, either the default resource schema (must exist).
* If loaded, return the form schema, which provides better information on writable status
* and contains available values.
*/
public get schema():SchemaResource {
return (this.form || this.resource).schema;
}
}

@ -0,0 +1,121 @@
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource";
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {ChangeMap, Changeset} from "core-app/modules/fields/changeset/changeset";
export abstract class ResourceChangeset<T extends HalResource|{ [key:string]:unknown; }> {
/** Maintain a single change set while editing */
protected changeset = new Changeset();
/** The projected resource, which will proxy values from the change set */
public projectedResource = new Proxy(
this.pristineResource,
{
get: (_, key:string) => this.proxyGet(key),
set: (_, key:string, val:any) => {
this.setValue(key, val);
return true;
},
}
);
public form:FormResource|null;
constructor(public pristineResource:T, form?:FormResource) {
this.form = form || null;
}
/**
* Return whether no changes were made to the work package
*/
public isEmpty() {
return this.changeset.changed.length === 0;
}
/**
* Return a shallow copy of the changes
*/
public get changes():ChangeMap {
return { ...this.changeset.all };
}
/**
* Return the changed attributes in this change;
*/
public get changedAttributes():string[] {
return this.changeset.changed;
}
/**
* Returns whether the given attribute was changed
*/
public contains(key:string) {
return this.changeset.contains(key);
}
/**
* Proxy getters to base or changeset.
* Special case for schema , which is overridden.
* @param key
*/
private proxyGet(key:string) {
if (key === 'schema') {
return this.schema;
}
return this.value(key);
}
/**
* Retrieve the editing value for the given attribute
*
* @param {string} key The attribute to read
* @return {any} Either the value from the overriden change, or the default value
*/
public value(key:string) {
if (this.isOverridden(key)) {
return this.changes[key];
} else {
return this.pristineResource[key];
}
}
public setValue(key:string, val:any) {
this.changes[key] = val;
}
public getSchemaName(attribute:string):string {
return attribute;
}
public clear() {
this.changeset.clear();
this.form = null;
}
/**
* Reset the given changed attribute
* @param key
*/
public reset(key:string) {
this.changeset.reset(key);
}
/**
* Return whether a change value exist for the given attribute key.
* @param {string} key
* @return {boolean}
*/
public isOverridden(key:string) {
return this.changes.hasOwnProperty(key);
}
/**
* Get the best schema currently available, either the default resource schema (must exist).
* If loaded, return the form schema, which provides better information on writable status
* and contains available values.
*/
public get schema():SchemaResource {
return (this.form || this.pristineResource).schema;
}
}

@ -27,16 +27,16 @@
// ++
import {Field, IFieldSchema} from "core-app/modules/fields/field.base";
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {DisplayFieldContext} from "core-app/modules/fields/display/display-field.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
export const cssClassCustomOption = 'custom-option';
export class DisplayField extends Field {
public static type:string;
public mode:string | null = null;
public changeset:WorkPackageChangeset|null = null;
public activeChange:WorkPackageChangeset|null = null;
protected I18n:I18nService = this.$injector.get(I18nService);
@ -77,8 +77,8 @@ export class DisplayField extends Field {
return null;
}
if (this.changeset) {
return this.changeset.value(this.name);
if (this.activeChange) {
return this.activeChange.projectedResource[this.name];
}
else {
return this.attribute;

@ -40,12 +40,11 @@ import {
} 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 {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 {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";
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
export const OpEditingPortalSchemaToken = new InjectionToken('wp-editing-portal--schema');
@ -66,34 +65,31 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
constructor(readonly I18n:I18nService,
readonly elementRef:ElementRef,
@Optional() @Inject(IWorkPackageEditingServiceToken) wpEditing:WorkPackageEditingService,
@Inject(OpEditingPortalChangesetToken) protected changeset:EditChangeset<HalResource>,
@Inject(OpEditingPortalChangesetToken) protected change:WorkPackageChangeset,
@Inject(OpEditingPortalSchemaToken) public schema:IFieldSchema,
@Inject(OpEditingPortalHandlerToken) readonly handler:EditFieldHandler,
readonly cdRef:ChangeDetectorRef,
readonly injector:Injector) {
super();
this.schema = this.schema || this.changeset.schema[this.name];
this.schema = this.schema || this.change.schema[this.name];
if (wpEditing) {
wpEditing.state(this.changeset.resource.id!)
if (this.change.state) {
this.change.state
.values$()
.pipe(
untilComponentDestroyed(this)
)
.subscribe((changeset) => {
if (this.changeset.form) {
const fieldSchema = changeset.schema[this.name];
if (!fieldSchema) {
return handler.deactivate(false);
}
this.changeset = changeset;
this.schema = this.changeset.schema[this.name];
this.initialize();
this.cdRef.markForCheck();
.subscribe((change) => {
const fieldSchema = change.schema[this.name];
if (!fieldSchema) {
return handler.deactivate(false);
}
this.change = change;
this.schema = change.schema[this.name];
this.initialize();
this.cdRef.markForCheck();
});
}
}
@ -122,17 +118,17 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
}
public get value() {
return this.changeset.value(this.name);
return this.resource[this.name];
}
public get name() {
// 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);
return this.change.getSchemaName(this.handler.fieldName);
}
public set value(value:any) {
this.changeset.setValue(this.name, this.parseValue(value));
this.resource[this.name] = this.parseValue(value);
}
public get placeholder() {
@ -144,7 +140,7 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
}
public get resource() {
return this.changeset.resource;
return this.change.projectedResource;
}
/**

@ -17,9 +17,9 @@ import {
} from "core-app/modules/fields/edit/edit-field.component";
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";
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
@Component({
@ -28,13 +28,13 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource";
})
export class EditFormPortalComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() schemaInput:IFieldSchema;
@Input() changesetInput:EditChangeset<HalResource|{ [key:string]:unknown; }>;
@Input() changeInput:WorkPackageChangeset;
@Input() editFieldHandler:EditFieldHandler;
@Output() public onEditFieldReady = new EventEmitter<void>();
public handler:EditFieldHandler;
public schema:IFieldSchema;
public changeset:EditChangeset<HalResource|{ [key:string]:unknown; }>;
public change:WorkPackageChangeset;
public fieldInjector:Injector;
public componentClass:IEditFieldType;
@ -50,16 +50,16 @@ export class EditFormPortalComponent implements OnInit, OnDestroy, AfterViewInit
if (this.editFieldHandler && this.schemaInput) {
this.handler = this.editFieldHandler;
this.schema = this.schemaInput;
this.changeset = this.changesetInput;
this.change = this.changeInput;
} else {
this.handler = this.injector.get<EditFieldHandler>(OpEditingPortalHandlerToken);
this.schema = this.injector.get<IFieldSchema>(OpEditingPortalSchemaToken);
this.changeset = this.injector.get<EditChangeset<HalResource|{ [key:string]:unknown; }>>(OpEditingPortalChangesetToken);
this.change = this.injector.get<WorkPackageChangeset>(OpEditingPortalChangesetToken);
}
this.componentClass = this.editField.getClassFor(this.handler.fieldName, this.schema.type);
this.fieldInjector = createLocalInjector(this.injector, this.changeset, this.handler, this.schema);
this.fieldInjector = createLocalInjector(this.injector, this.change, this.handler, this.schema);
}
ngOnDestroy() {

@ -7,18 +7,17 @@ import {
import {PortalInjector} from "@angular/cdk/portal";
import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler";
import {IFieldSchema} from "core-app/modules/fields/field.base";
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
/**
* Creates an injector for the edit field portal to pass data into.
*
* @returns {PortalInjector}
*/
export function createLocalInjector(injector:Injector, changeset:EditChangeset<HalResource|{ [key:string]:unknown; }>, fieldHandler:EditFieldHandler, schema:IFieldSchema):Injector {
export function createLocalInjector(injector:Injector, change:WorkPackageChangeset, fieldHandler:EditFieldHandler, schema:IFieldSchema):Injector {
const injectorTokens = new WeakMap();
injectorTokens.set(OpEditingPortalChangesetToken, changeset);
injectorTokens.set(OpEditingPortalChangesetToken, change);
injectorTokens.set(OpEditingPortalHandlerToken, fieldHandler);
injectorTokens.set(OpEditingPortalSchemaToken, schema);

@ -48,7 +48,7 @@ export class WorkPackageEditingPortalService {
.subscribe(() => outlet.detach());
// Create an injector that contains injectable reference to the edit field and handler
const localInjector = createLocalInjector(injector, form.changeset, fieldHandler, schema);
const localInjector = createLocalInjector(injector, form.change, fieldHandler, schema);
// Create a portal for the edit-form/field
const portal = new ComponentPortal(EditFormPortalComponent, null, localInjector);

@ -83,7 +83,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
}
public get value() {
const val = this.changeset.value(this.name);
const val = this.resource[this.name];
return val ? val[0] : val;
}
@ -93,7 +93,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
* @returns {any}
*/
public buildSelectedOption() {
const value:HalResource[] = this.changeset.value(this.name);
const value:HalResource[] = this.resource[this.name];
return value ? value.map(val => this.findValueOption(val)) : [];
}
@ -119,8 +119,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
return option;
};
const value = _.castArray(val).map(el => mapper(el));
this.changeset.setValue(this.name, value);
this.resource[this.name] = _.castArray(val).map(el => mapper(el));
}
public onOpen() {
@ -159,8 +158,8 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
private setValues(availableValues:any[], sortValuesByName:boolean = false) {
if (sortValuesByName) {
availableValues.sort(function (a:any, b:any) {
var nameA = a.name.toLowerCase();
var nameB = b.name.toLowerCase();
let nameA = a.name.toLowerCase();
let nameB = b.name.toLowerCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
});
}

@ -100,7 +100,7 @@ export class GridAreaService {
public saveWidgetChangeset(changeset:WidgetChangeset) {
let payload = this.gridDm.extractPayload(this.resource, this.schema);
let payloadWidget = payload.widgets.find((w:any) => w.id === changeset.resource.id);
let payloadWidget = payload.widgets.find((w:any) => w.id === changeset.pristineResource.id);
Object.assign(payloadWidget, changeset.changes);
// Adding the id so that the url can be deduced

@ -46,7 +46,7 @@ export abstract class AbstractWidgetComponent {
protected injector:Injector) { }
protected setChangesetOptions(values:{ [key:string]:unknown; }) {
let changeset = new WidgetChangeset(this.injector, this.resource);
let changeset = new WidgetChangeset(this.resource);
changeset.setValue('options', Object.assign({}, this.resource.options, values));

@ -1,5 +1,5 @@
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
export class CustomTextChangeset extends EditChangeset<{ [key:string]:unknown; }> {
export class CustomTextChangeset extends ResourceChangeset<{ [key:string]:unknown; }> {
}

@ -4,7 +4,6 @@ import {IFieldSchema} from "core-app/modules/fields/field.base";
import {BehaviorSubject} from "rxjs";
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource";
import {CustomTextChangeset} from "core-app/modules/grids/widgets/custom-text/custom-text-changeset";
import {Attachable} from "core-app/modules/hal/resources/mixins/attachable-mixin";
import {UploadFile} from "core-components/api/op-file-upload/op-file-upload.service";
@Injectable()
@ -30,12 +29,12 @@ export class CustomTextEditFieldService extends EditFieldHandler {
}
public initialize(value:GridWidgetResource) {
this.changeset = new CustomTextChangeset(this.injector, this.newEditResource(value));
this.changeset = new CustomTextChangeset(this.newEditResource(value));
this.valueChanged$ = new BehaviorSubject(value.options['text'] as string);
}
public reinitialize(value:GridWidgetResource) {
this.changeset = new CustomTextChangeset(this.injector, this.newEditResource(value));
this.changeset = new CustomTextChangeset(this.newEditResource(value));
}
/**
@ -111,7 +110,7 @@ export class CustomTextEditFieldService extends EditFieldHandler {
}
isChanged():boolean {
return !this.changeset.empty;
return !this.changeset.isEmpty();
}
stopPropagation(evt:JQuery.TriggeredEvent):boolean {

@ -24,7 +24,7 @@
<div class="wp-edit-field inplace-edit">
<edit-form-portal *ngIf="active"
[schemaInput]="schema"
[changesetInput]="changeset"
[changeInput]="changeset"
[editFieldHandler]="handler">
</edit-form-portal>
<attachments *ngIf="active"

@ -1,6 +1,6 @@
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource";
export class WidgetChangeset extends EditChangeset<GridWidgetResource> {
export class WidgetChangeset extends ResourceChangeset<GridWidgetResource> {
}

@ -43,8 +43,8 @@ import {LoadingIndicatorService} from 'core-app/modules/common/loading-indicator
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {StateService} from "@uirouter/core";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/op-file-upload.service";
import {WorkPackageCreateService} from 'core-app/components/wp-new/wp-create.service';
import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service";
describe('WorkPackage', () => {
@ -81,7 +81,7 @@ describe('WorkPackage', () => {
PathHelperService,
I18nService,
{ provide: WorkPackageDmService, useValue: {} },
{ provide: IWorkPackageCreateServiceToken, useValue: {} },
{ provide: WorkPackageCreateService, useValue: {} },
{ provide: StateService, useValue: {} },
{ provide: SchemaCacheService, useValue: {} },
]

@ -80,7 +80,7 @@ export interface WorkPackageResourceLinks extends WorkPackageResourceEmbedded {
addChild(child:HalResource):Promise<any>;
addComment(comment:{ comment:string }, headers?:any):Promise<any>;
addComment(comment:unknown, headers?:any):Promise<any>;
addRelation(relation:any):Promise<any>;
@ -121,7 +121,7 @@ export class WorkPackageBaseResource extends HalResource {
public activities:CollectionResource;
public attachments:AttachmentCollectionResource;
public overriddenSchema?:SchemaResource;
public overriddenSchema:SchemaResource|undefined = undefined;
public __initialized_at:Number;
readonly I18n:I18nService = this.injector.get(I18nService);

@ -54,6 +54,7 @@ import {WikiPageResource} from "core-app/modules/hal/resources/wiki-page-resourc
import {MeetingContentResource} from "core-app/modules/hal/resources/meeting-content-resource";
import {PostResource} from "core-app/modules/hal/resources/post-resource";
import {StatusResource} from "core-app/modules/hal/resources/status-resource";
import {AttachmentCollectionResource} from "core-app/modules/hal/resources/attachment-collection-resource";
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource";
import {GridResource} from "core-app/modules/hal/resources/grid-resource";
import {TimeEntryResource} from "core-app/modules/hal/resources/time-entry-resource";
@ -124,6 +125,9 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter
WorkPackageCollection: {
cls: WorkPackageCollectionResource
},
AttachmentCollection: {
cls: AttachmentCollectionResource
},
Query: {
cls: QueryResource,
attrTypes: {
@ -131,7 +135,16 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter
}
},
Form: {
cls: FormResource
cls: FormResource,
attrTypes: {
payload: 'FormPayload'
}
},
FormPayload: {
cls: HalResource,
attrTypes: {
attachments: 'AttachmentsCollection'
}
},
QueryFilterInstance: {
cls: QueryFilterInstanceResource,

@ -160,6 +160,8 @@ import {WorkPackageViewToggleButton} from "core-components/wp-buttons/wp-view-to
import {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.component";
import {WorkPackageViewDropdownMenuDirective} from "core-components/op-context-menu/handlers/wp-view-dropdown-menu.directive";
import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service";
@NgModule({
@ -201,6 +203,12 @@ import {WorkPackageEventsService} from "core-app/modules/work_packages/events/wo
WorkPackageStaticQueriesService,
WorkPackagesListInvalidQueryService,
// Provide a separate service for creation events of WP Inline create
// This can be hierarchically injected to provide isolated events on an embedded table
WorkPackageRelationsService,
WorkPackageCacheService,
SchemaCacheService,
KeepTabService,
WorkPackageNotificationService,
WorkPackageDmService,

@ -41,11 +41,9 @@ import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/ro
import {WorkPackageViewSumService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-sum.service";
import {WorkPackageViewAdditionalElementsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service";
import {WorkPackageViewHighlightingService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-highlighting.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service";
import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service";
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 {WorkPackagesListService} from "core-components/wp-list/wp-list.service";
import {WorkPackageService} from "core-components/work-packages/work-package.service";
@ -95,9 +93,8 @@ export const WpIsolatedGraphQuerySpaceProviders = [
WpChildrenInlineCreateService,
WpRelationInlineCreateService,
// Provide both serves with tokens to avoid tight dependency cycles
{ provide: IWorkPackageCreateServiceToken, useClass: WorkPackageCreateService },
{ provide: IWorkPackageEditingServiceToken, useClass: WorkPackageEditingService },
WorkPackageCreateService,
WorkPackageEditingService,
WorkPackageStatesInitializationService,
@ -121,10 +118,4 @@ export const WpIsolatedGraphQuerySpaceProviders = [
providers: WpIsolatedGraphQuerySpaceProviders
})
export class WorkPackageIsolatedGraphQuerySpaceDirective extends WorkPackageIsolatedQuerySpaceDirective {
//constructor(private elementRef:ElementRef,
// public querySpace:IsolatedQuerySpace,
// private injector:Injector) {
// debugLog("Opening isolated query space %O in %O", injector, elementRef.nativeElement);
//}
}

@ -41,11 +41,9 @@ import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/ro
import {WorkPackageViewSumService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-sum.service";
import {WorkPackageViewAdditionalElementsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service";
import {WorkPackageViewHighlightingService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-highlighting.service";
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service";
import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service";
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 {WorkPackagesListService} from "core-components/wp-list/wp-list.service";
import {WorkPackageService} from "core-components/work-packages/work-package.service";
@ -113,9 +111,8 @@ import {WorkPackageViewHierarchyIdentationService} from "core-app/modules/work_p
WorkPackageCardViewService,
// Provide both serves with tokens to avoid tight dependency cycles
{ provide: IWorkPackageCreateServiceToken, useClass: WorkPackageCreateService },
{ provide: IWorkPackageEditingServiceToken, useClass: WorkPackageEditingService },
WorkPackageCreateService,
WorkPackageEditingService,
WorkPackageStatesInitializationService,
PortalCleanupService,

@ -29,14 +29,14 @@
<wp-filter-button>
</wp-filter-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<wp-details-view-button>
</wp-details-view-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<wp-view-toggle-button>
</wp-view-toggle-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<wp-details-view-button>
</wp-details-view-button>
</li>
<li class="toolbar-item hidden-for-mobile -no-spacing">
<wp-timeline-toggle-button>
</wp-timeline-toggle-button>

@ -39,12 +39,8 @@ import {AuthorisationService} from "core-app/modules/common/model-auth/model-aut
import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service";
import {States} from "core-components/states.service";
import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service";
import {
IWorkPackageEditingService,
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 {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
export class WorkPackageSingleViewBase implements OnDestroy {
public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService);
@ -52,7 +48,7 @@ export class WorkPackageSingleViewBase implements OnDestroy {
public I18n:I18nService = this.injector.get(I18nService);
public keepTab:KeepTabService = this.injector.get(KeepTabService);
public PathHelper:PathHelperService = this.injector.get(PathHelperService);
protected wpEditing:IWorkPackageEditingService = this.injector.get(IWorkPackageEditingServiceToken);
protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService);
protected wpTableFocus:WorkPackageViewFocusService = this.injector.get(WorkPackageViewFocusService);
protected wpNotifications:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService);
protected projectCacheService:ProjectCacheService = this.injector.get(ProjectCacheService);

Loading…
Cancel
Save