diff --git a/frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts b/frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts index 4011c68079..c8ba7195cf 100644 --- a/frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts +++ b/frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts @@ -143,10 +143,7 @@ export class WorkPackageResource extends HalResource { } public get isMilestone(): boolean { - /** - * it would be better if this was not deduced but rather taken from the type - */ - return this.hasOwnProperty('date'); + return this.schema.hasOwnProperty('date'); } /** diff --git a/frontend/app/components/routing/wp-list/wp-list.controller.ts b/frontend/app/components/routing/wp-list/wp-list.controller.ts index a218eeb904..db2e909e16 100644 --- a/frontend/app/components/routing/wp-list/wp-list.controller.ts +++ b/frontend/app/components/routing/wp-list/wp-list.controller.ts @@ -254,6 +254,8 @@ function WorkPackagesListController($scope, var staleRow = rowLookup[fresh.id]; if (staleRow && staleRow.object.lockVersion === fresh.lockVersion) { json.work_packages[i] = staleRow.object; + } else { + wpCacheService.updateWorkPackage(fresh); } }); diff --git a/frontend/app/components/work-packages/work-package-cache.service.ts b/frontend/app/components/work-packages/work-package-cache.service.ts index 2ad5d7e29c..4120321113 100644 --- a/frontend/app/components/work-packages/work-package-cache.service.ts +++ b/frontend/app/components/work-packages/work-package-cache.service.ts @@ -1,4 +1,3 @@ -import {WorkPackagesListService} from './../wp-list/wp-list.service'; // -- copyright // OpenProject is a project management system. // Copyright (C) 2012-2015 the OpenProject Foundation (OPF) @@ -32,6 +31,7 @@ import {opWorkPackagesModule} from "../../angular-modules"; import {WorkPackageResource} from "../api/api-v3/hal-resources/work-package-resource.service"; import {SchemaResource} from './../api/api-v3/hal-resources/schema-resource.service'; import {ApiWorkPackagesService} from "../api/api-work-packages/api-work-packages.service"; +import {WorkPackageNotificationService} from './../wp-edit/wp-notification.service'; import {State} from "../../helpers/reactive-fassade"; import IScope = angular.IScope; import {States} from "../states.service"; @@ -47,7 +47,9 @@ export class WorkPackageCacheService { /*@ngInject*/ constructor(private states: States, + private $rootScope: ng.IRootScopeService, private $q: ng.IQService, + private wpNotificationsService:WorkPackageNotificationService, private apiWorkPackages: ApiWorkPackagesService) { } @@ -79,6 +81,21 @@ export class WorkPackageCacheService { } } + saveIfChanged(workPackage) { + if (!(workPackage.dirty || workPackage.isNew)) { + return this.$q.when(workPackage); + } + + return workPackage.save() + .then(() => { + this.wpNotificationsService.showSave(workPackage); + this.$rootScope.$emit('workPackagesRefreshInBackground'); + }) + .catch((error) => { + this.wpNotificationsService.handleErrorResponse(error, workPackage); + }); + } + loadWorkPackage(workPackageId: number, forceUpdate = false): State { const state = this.states.workPackages.get(getWorkPackageId(workPackageId)); if (forceUpdate) { diff --git a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts index 3186714318..166d54f9c2 100644 --- a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts +++ b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts @@ -1,17 +1,13 @@ -import {WorkPackageResourceInterface} from "./../../../api/api-v3/hal-resources/work-package-resource.service"; -import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from "./../wp-timeline"; +import {WorkPackageResourceInterface} from './../../../api/api-v3/hal-resources/work-package-resource.service'; +import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from './../wp-timeline'; const classNameLeftHandle = "leftHandle"; const classNameRightHandle = "rightHandle"; -interface CellDateMovement extends Object { +interface CellDateMovement { // Target values to move work package to startDate?: moment.Moment; dueDate?: moment.Moment; - - // Original values once cell was clicked - initialStartDate?: moment.Moment; - initialDueDate?: moment.Moment; } export class TimelineCellRenderer { @@ -38,17 +34,18 @@ export class TimelineCellRenderer { * Restore the original date, if any was set. */ public onCancel(wp:WorkPackageResourceInterface, dates:CellDateMovement) { - this.assignDate(wp, 'startDate', dates.initialStartDate); - this.assignDate(wp, 'dueDate', dates.initialDueDate); + wp.restoreFromPristine('startDate'); + wp.restoreFromPristine('dueDate'); } /** * Handle movement by days of the work package to either (or both) edge(s) * depending on which initial date was set. */ - public onDaysMoved(dates:CellDateMovement, delta:number) { - const initialStartDate = dates.initialStartDate; - const initialDueDate = dates.initialDueDate; + public onDaysMoved(wp:WorkPackageResourceInterface, delta:number) { + const initialStartDate = wp.$pristine['startDate']; + const initialDueDate = wp.$pristine['dueDate']; + let dates:CellDateMovement = {}; if (initialStartDate) { dates.startDate = moment(initialStartDate).add(delta, "days"); @@ -74,27 +71,32 @@ export class TimelineCellRenderer { } if (!jQuery(ev.target).hasClass(classNameRightHandle)) { - dates.initialStartDate = moment(renderInfo.workPackage.startDate); + renderInfo.workPackage.storePristine('startDate'); } if (!jQuery(ev.target).hasClass(classNameLeftHandle)) { - dates.initialDueDate = moment(renderInfo.workPackage.dueDate); + renderInfo.workPackage.storePristine('dueDate'); } return dates; } /** - * @return true, if the element should still be displayed. - * false, if the element must be removed from the timeline. + * Decide whether we need to render anything for the work package. */ - public update(element: HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo: RenderInfo): boolean { + public willRender(renderInfo):boolean { + const wp = renderInfo.workPackage; + return !!(wp.startDate || wp.dueDate) + } + + public update(element:HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo:RenderInfo) { // abort if no start or due date if (!wp.startDate || !wp.dueDate) { - return false; + return; } // general settings - bar element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; + element.style.backgroundColor = this.typeColor(renderInfo.workPackage); const viewParams = renderInfo.viewParams; const start = moment(wp.startDate as any); @@ -107,8 +109,6 @@ export class TimelineCellRenderer { // duration const duration = due.diff(start, "days") + 1; element.style.width = calculatePositionValueForDayCount(viewParams, duration); - - return true; } /** @@ -121,7 +121,6 @@ export class TimelineCellRenderer { bar.className = timelineElementCssClass + " " + this.type; bar.style.position = "relative"; bar.style.height = "1em"; - bar.style.backgroundColor = this.typeColor(renderInfo.workPackage as any); bar.style.borderRadius = "2px"; bar.style.cssFloat = "left"; bar.style.zIndex = "50"; @@ -130,7 +129,6 @@ export class TimelineCellRenderer { const left = document.createElement("div"); left.className = classNameLeftHandle; left.style.position = "absolute"; - left.style.backgroundColor = "red"; left.style.left = "0px"; left.style.top = "0px"; left.style.width = "20px"; @@ -142,14 +140,13 @@ export class TimelineCellRenderer { const right = document.createElement("div"); right.className = classNameRightHandle; right.style.position = "absolute"; - right.style.backgroundColor = "green"; right.style.right = "0px"; right.style.top = "0px"; right.style.width = "20px"; right.style.maxWidth = "20%"; right.style.height = "100%"; right.style.cursor = "e-resize"; - bar.appendChild(right); + bar.appendChild(right) return bar; } @@ -176,4 +173,4 @@ export class TimelineCellRenderer { jQuery(".hascontextmenu").css("cursor", cursor); jQuery("." + timelineElementCssClass).css("cursor", cursor); } -} +} \ No newline at end of file diff --git a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts index 5c88b29aaf..453bdfa31a 100644 --- a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts +++ b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts @@ -2,12 +2,9 @@ import {WorkPackageResourceInterface} from './../../../api/api-v3/hal-resources/ import {TimelineCellRenderer} from './timeline-cell-renderer'; import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from './../wp-timeline'; -interface CellMilestoneMovement extends Object { +interface CellMilestoneMovement { // Target value to move milestone to date?: moment.Moment; - - // Original values once cell was clicked - initialDate?: moment.Moment; } export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { @@ -32,14 +29,15 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { * Restore the original date, if any was set. */ public onCancel(wp: WorkPackageResourceInterface, dates:CellMilestoneMovement) { - this.assignDate(wp, 'date', dates.initialDate); + wp.restoreFromPristine('date'); } /** * Handle movement by days of milestone. */ - public onDaysMoved(dates:CellMilestoneMovement, delta:number) { - const initialDate = dates.initialDate; + public onDaysMoved(wp:WorkPackageResourceInterface, delta:number) { + const initialDate = wp.$pristine['date']; + let dates:CellMilestoneMovement = {}; if (initialDate) { dates.date = moment(initialDate).add(delta, "days"); @@ -52,19 +50,26 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { let dates:CellMilestoneMovement = {}; this.forceCursor('ew-resize'); - dates.initialDate = moment(renderInfo.workPackage.date); + renderInfo.workPackage.storePristine('date'); return dates; } - public update(element:HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo:RenderInfo): boolean { + public willRender(renderInfo):boolean { + const wp = renderInfo.workPackage; + return !!wp.date; + } + + public update(element:HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo:RenderInfo) { // abort if no start or due date if (!wp.date) { - return false; + return; } element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; + element.style.backgroundColor = this.typeColor(renderInfo.workPackage); element.style.width = '1em'; + element.style.height = '1em'; const viewParams = renderInfo.viewParams; const date = moment(wp.date as any); @@ -72,8 +77,6 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { // offset left const offsetStart = date.diff(viewParams.dateDisplayStart, "days"); element.style.left = calculatePositionValueForDayCount(viewParams, offsetStart); - - return true; } /** @@ -85,14 +88,12 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { el.className = timelineElementCssClass + " " + this.type; el.style.position = "relative"; - el.style.height = "1em"; - el.style.backgroundColor = this.typeColor(renderInfo.workPackage as any); el.style.borderRadius = "2px"; el.style.zIndex = "50"; el.style.cursor = "ew-resize"; el.style.transform = 'rotate(45deg)'; - el.style.transformOrigin = '75% 100%'; + el.style.transformOrigin = 'center center'; return el; } -} +} \ No newline at end of file diff --git a/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts b/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts index c703659e04..43d7abf911 100644 --- a/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts +++ b/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts @@ -64,7 +64,9 @@ export function registerWorkPackageMouseHandler(this: void, // Let the renderer decide which fields we change renderer.assignDateValues(wp, dates); - wpCacheService.updateWorkPackage(wp as any); + + // Update the work package to refresh dates columns + wpCacheService.updateWorkPackage(wp); } function mouseMoveFn(ev: JQueryEventObject) { @@ -72,7 +74,7 @@ export function registerWorkPackageMouseHandler(this: void, const distance = Math.floor((mev.clientX - startX) / renderInfo.viewParams.pixelPerDay); const days = distance < 0 ? distance + 1 : distance; - dateStates = renderer.onDaysMoved(dateStates, days); + dateStates = renderer.onDaysMoved(renderInfo.workPackage, days); applyDateValues(dateStates); } @@ -106,6 +108,9 @@ export function registerWorkPackageMouseHandler(this: void, if (cancelled) { renderer.onCancel(renderInfo.workPackage, dateStates); + } else { + // Persist the changes + wpCacheService.saveIfChanged(renderInfo.workPackage); } jBody.off("mousemove", mouseMoveFn); @@ -119,6 +124,7 @@ export function registerWorkPackageMouseHandler(this: void, dateStates = {}; workPackageTimeline.refreshView(); + } } diff --git a/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts b/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts index 444ed9d0aa..324617aec8 100644 --- a/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts +++ b/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts @@ -76,14 +76,21 @@ export class WorkPackageTimelineCell { } private lazyInit(renderer: TimelineCellRenderer, renderInfo: RenderInfo) { + const wasRendered = this.element !== null && this.element.parentNode; + + // Remove the element if it should no longer be rendered at the moment + if (wasRendered && !renderer.willRender(renderInfo)) { + this.element.parentNode.removeChild(this.element); + return; + } // If already rendered with correct shape, ignore - if (this.element !== null && (this.elementShape === renderer.type)) { + if (wasRendered && (this.elementShape === renderer.type)) { return; } // Remove the element first if we're redrawing - if (this.element !== null) { + if (wasRendered) { this.element.parentNode.removeChild(this.element); } diff --git a/frontend/app/components/wp-table/timeline/wp-timeline.header.ts b/frontend/app/components/wp-table/timeline/wp-timeline.header.ts index f01d7327bc..1f7b752f2f 100644 --- a/frontend/app/components/wp-table/timeline/wp-timeline.header.ts +++ b/frontend/app/components/wp-table/timeline/wp-timeline.header.ts @@ -91,17 +91,11 @@ export class WpTimelineHeader { this.scrollBar = this.outerHeader.find('.wp-timeline--slider'); this.scrollBar.slider({ min: 0, - max: 50, // TODO change to actual max slide: (evt, ui) => { this.wpTimeline.viewParameterSettings.scrollOffsetInDays = -ui.value; - this.wpTimeline.refreshScrollOnly(); - - // The handle uses left offset to set the current position. - // With different widths, this causes the slider to move outside the container - // which we can control through and additional margin-left. - let currentMax = this.scrollBar.slider('option', 'max'); - let handleOffset = this.scrollBarHandle.width() * (ui.value / currentMax); - this.scrollBarHandle.css('margin-left', -1 * handleOffset); + this.wpTimeline.refreshScrollOnly(); + + this.recalculateScrollBarLeftMargin(ui.value); } }); @@ -109,11 +103,28 @@ export class WpTimelineHeader { } private updateScrollbar(vp: TimelineViewParameters) { - this.scrollBar.slider('option', 'max', vp.maxSteps); - - // Set width of handle - let newWidth = Math.max((vp.maxSteps / vp.pixelPerDay), 20) + 'px'; + let maxWidth = this.scrollBar.width(), + daysDisplayed = Math.min(vp.maxSteps, Math.floor(maxWidth / vp.pixelPerDay)), + newMax = vp.maxSteps - daysDisplayed, + newWidth = Math.max(Math.min(maxWidth, (vp.maxSteps / vp.pixelPerDay)), 20) + 'px', + currentValue = this.scrollBar.slider('option', 'value'), + newValue = Math.min(newMax, currentValue); + + this.scrollBar.slider('option', 'max', newMax); this.scrollBarHandle.css('width', newWidth); + this.scrollBar.slider('option', 'value', newValue); + + this.recalculateScrollBarLeftMargin(newValue); + } + + // The handle uses left offset to set the current position. + // With different widths, this causes the slider to move outside the container + // which we can control through and additional margin-left. + private recalculateScrollBarLeftMargin(value) { + let currentMax = this.scrollBar.slider('option', 'max'); + + let handleOffset = (currentMax === 0) ? 0 : this.scrollBarHandle.width() * (value / currentMax); + this.scrollBarHandle.css('margin-left', -1 * handleOffset); } private lazyInit() {