Merge remote-tracking branch 'op/timeline' into timeline

# Conflicts:
#	frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts
#	frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts
pull/5162/head
Roman Roelofsen 8 years ago
commit de23a7abd3
  1. 5
      frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts
  2. 2
      frontend/app/components/routing/wp-list/wp-list.controller.ts
  3. 19
      frontend/app/components/work-packages/work-package-cache.service.ts
  4. 47
      frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts
  5. 33
      frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts
  6. 10
      frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts
  7. 11
      frontend/app/components/wp-table/timeline/wp-timeline-cell.ts
  8. 37
      frontend/app/components/wp-table/timeline/wp-timeline.header.ts

@ -143,10 +143,7 @@ export class WorkPackageResource extends HalResource {
} }
public get isMilestone(): boolean { public get isMilestone(): boolean {
/** return this.schema.hasOwnProperty('date');
* it would be better if this was not deduced but rather taken from the type
*/
return this.hasOwnProperty('date');
} }
/** /**

@ -254,6 +254,8 @@ function WorkPackagesListController($scope,
var staleRow = rowLookup[fresh.id]; var staleRow = rowLookup[fresh.id];
if (staleRow && staleRow.object.lockVersion === fresh.lockVersion) { if (staleRow && staleRow.object.lockVersion === fresh.lockVersion) {
json.work_packages[i] = staleRow.object; json.work_packages[i] = staleRow.object;
} else {
wpCacheService.updateWorkPackage(fresh);
} }
}); });

@ -1,4 +1,3 @@
import {WorkPackagesListService} from './../wp-list/wp-list.service';
// -- copyright // -- copyright
// OpenProject is a project management system. // OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) // 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 {WorkPackageResource} from "../api/api-v3/hal-resources/work-package-resource.service";
import {SchemaResource} from './../api/api-v3/hal-resources/schema-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 {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 {State} from "../../helpers/reactive-fassade";
import IScope = angular.IScope; import IScope = angular.IScope;
import {States} from "../states.service"; import {States} from "../states.service";
@ -47,7 +47,9 @@ export class WorkPackageCacheService {
/*@ngInject*/ /*@ngInject*/
constructor(private states: States, constructor(private states: States,
private $rootScope: ng.IRootScopeService,
private $q: ng.IQService, private $q: ng.IQService,
private wpNotificationsService:WorkPackageNotificationService,
private apiWorkPackages: ApiWorkPackagesService) { 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<WorkPackageResource> { loadWorkPackage(workPackageId: number, forceUpdate = false): State<WorkPackageResource> {
const state = this.states.workPackages.get(getWorkPackageId(workPackageId)); const state = this.states.workPackages.get(getWorkPackageId(workPackageId));
if (forceUpdate) { if (forceUpdate) {

@ -1,17 +1,13 @@
import {WorkPackageResourceInterface} from "./../../../api/api-v3/hal-resources/work-package-resource.service"; import {WorkPackageResourceInterface} from './../../../api/api-v3/hal-resources/work-package-resource.service';
import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from "./../wp-timeline"; import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from './../wp-timeline';
const classNameLeftHandle = "leftHandle"; const classNameLeftHandle = "leftHandle";
const classNameRightHandle = "rightHandle"; const classNameRightHandle = "rightHandle";
interface CellDateMovement extends Object { interface CellDateMovement {
// Target values to move work package to // Target values to move work package to
startDate?: moment.Moment; startDate?: moment.Moment;
dueDate?: moment.Moment; dueDate?: moment.Moment;
// Original values once cell was clicked
initialStartDate?: moment.Moment;
initialDueDate?: moment.Moment;
} }
export class TimelineCellRenderer { export class TimelineCellRenderer {
@ -38,17 +34,18 @@ export class TimelineCellRenderer {
* Restore the original date, if any was set. * Restore the original date, if any was set.
*/ */
public onCancel(wp:WorkPackageResourceInterface, dates:CellDateMovement) { public onCancel(wp:WorkPackageResourceInterface, dates:CellDateMovement) {
this.assignDate(wp, 'startDate', dates.initialStartDate); wp.restoreFromPristine('startDate');
this.assignDate(wp, 'dueDate', dates.initialDueDate); wp.restoreFromPristine('dueDate');
} }
/** /**
* Handle movement by <delta> days of the work package to either (or both) edge(s) * Handle movement by <delta> days of the work package to either (or both) edge(s)
* depending on which initial date was set. * depending on which initial date was set.
*/ */
public onDaysMoved(dates:CellDateMovement, delta:number) { public onDaysMoved(wp:WorkPackageResourceInterface, delta:number) {
const initialStartDate = dates.initialStartDate; const initialStartDate = wp.$pristine['startDate'];
const initialDueDate = dates.initialDueDate; const initialDueDate = wp.$pristine['dueDate'];
let dates:CellDateMovement = {};
if (initialStartDate) { if (initialStartDate) {
dates.startDate = moment(initialStartDate).add(delta, "days"); dates.startDate = moment(initialStartDate).add(delta, "days");
@ -74,27 +71,32 @@ export class TimelineCellRenderer {
} }
if (!jQuery(ev.target).hasClass(classNameRightHandle)) { if (!jQuery(ev.target).hasClass(classNameRightHandle)) {
dates.initialStartDate = moment(renderInfo.workPackage.startDate); renderInfo.workPackage.storePristine('startDate');
} }
if (!jQuery(ev.target).hasClass(classNameLeftHandle)) { if (!jQuery(ev.target).hasClass(classNameLeftHandle)) {
dates.initialDueDate = moment(renderInfo.workPackage.dueDate); renderInfo.workPackage.storePristine('dueDate');
} }
return dates; return dates;
} }
/** /**
* @return true, if the element should still be displayed. * Decide whether we need to render anything for the work package.
* false, if the element must be removed from the timeline.
*/ */
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 // abort if no start or due date
if (!wp.startDate || !wp.dueDate) { if (!wp.startDate || !wp.dueDate) {
return false; return;
} }
// general settings - bar // general settings - bar
element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px";
element.style.backgroundColor = this.typeColor(renderInfo.workPackage);
const viewParams = renderInfo.viewParams; const viewParams = renderInfo.viewParams;
const start = moment(wp.startDate as any); const start = moment(wp.startDate as any);
@ -107,8 +109,6 @@ export class TimelineCellRenderer {
// duration // duration
const duration = due.diff(start, "days") + 1; const duration = due.diff(start, "days") + 1;
element.style.width = calculatePositionValueForDayCount(viewParams, duration); element.style.width = calculatePositionValueForDayCount(viewParams, duration);
return true;
} }
/** /**
@ -121,7 +121,6 @@ export class TimelineCellRenderer {
bar.className = timelineElementCssClass + " " + this.type; bar.className = timelineElementCssClass + " " + this.type;
bar.style.position = "relative"; bar.style.position = "relative";
bar.style.height = "1em"; bar.style.height = "1em";
bar.style.backgroundColor = this.typeColor(renderInfo.workPackage as any);
bar.style.borderRadius = "2px"; bar.style.borderRadius = "2px";
bar.style.cssFloat = "left"; bar.style.cssFloat = "left";
bar.style.zIndex = "50"; bar.style.zIndex = "50";
@ -130,7 +129,6 @@ export class TimelineCellRenderer {
const left = document.createElement("div"); const left = document.createElement("div");
left.className = classNameLeftHandle; left.className = classNameLeftHandle;
left.style.position = "absolute"; left.style.position = "absolute";
left.style.backgroundColor = "red";
left.style.left = "0px"; left.style.left = "0px";
left.style.top = "0px"; left.style.top = "0px";
left.style.width = "20px"; left.style.width = "20px";
@ -142,14 +140,13 @@ export class TimelineCellRenderer {
const right = document.createElement("div"); const right = document.createElement("div");
right.className = classNameRightHandle; right.className = classNameRightHandle;
right.style.position = "absolute"; right.style.position = "absolute";
right.style.backgroundColor = "green";
right.style.right = "0px"; right.style.right = "0px";
right.style.top = "0px"; right.style.top = "0px";
right.style.width = "20px"; right.style.width = "20px";
right.style.maxWidth = "20%"; right.style.maxWidth = "20%";
right.style.height = "100%"; right.style.height = "100%";
right.style.cursor = "e-resize"; right.style.cursor = "e-resize";
bar.appendChild(right); bar.appendChild(right)
return bar; return bar;
} }
@ -176,4 +173,4 @@ export class TimelineCellRenderer {
jQuery(".hascontextmenu").css("cursor", cursor); jQuery(".hascontextmenu").css("cursor", cursor);
jQuery("." + timelineElementCssClass).css("cursor", cursor); jQuery("." + timelineElementCssClass).css("cursor", cursor);
} }
} }

@ -2,12 +2,9 @@ import {WorkPackageResourceInterface} from './../../../api/api-v3/hal-resources/
import {TimelineCellRenderer} from './timeline-cell-renderer'; import {TimelineCellRenderer} from './timeline-cell-renderer';
import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from './../wp-timeline'; import {RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass} from './../wp-timeline';
interface CellMilestoneMovement extends Object { interface CellMilestoneMovement {
// Target value to move milestone to // Target value to move milestone to
date?: moment.Moment; date?: moment.Moment;
// Original values once cell was clicked
initialDate?: moment.Moment;
} }
export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
@ -32,14 +29,15 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
* Restore the original date, if any was set. * Restore the original date, if any was set.
*/ */
public onCancel(wp: WorkPackageResourceInterface, dates:CellMilestoneMovement) { public onCancel(wp: WorkPackageResourceInterface, dates:CellMilestoneMovement) {
this.assignDate(wp, 'date', dates.initialDate); wp.restoreFromPristine('date');
} }
/** /**
* Handle movement by <delta> days of milestone. * Handle movement by <delta> days of milestone.
*/ */
public onDaysMoved(dates:CellMilestoneMovement, delta:number) { public onDaysMoved(wp:WorkPackageResourceInterface, delta:number) {
const initialDate = dates.initialDate; const initialDate = wp.$pristine['date'];
let dates:CellMilestoneMovement = {};
if (initialDate) { if (initialDate) {
dates.date = moment(initialDate).add(delta, "days"); dates.date = moment(initialDate).add(delta, "days");
@ -52,19 +50,26 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
let dates:CellMilestoneMovement = {}; let dates:CellMilestoneMovement = {};
this.forceCursor('ew-resize'); this.forceCursor('ew-resize');
dates.initialDate = moment(renderInfo.workPackage.date); renderInfo.workPackage.storePristine('date');
return dates; 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 // abort if no start or due date
if (!wp.date) { if (!wp.date) {
return false; return;
} }
element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px";
element.style.backgroundColor = this.typeColor(renderInfo.workPackage);
element.style.width = '1em'; element.style.width = '1em';
element.style.height = '1em';
const viewParams = renderInfo.viewParams; const viewParams = renderInfo.viewParams;
const date = moment(wp.date as any); const date = moment(wp.date as any);
@ -72,8 +77,6 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
// offset left // offset left
const offsetStart = date.diff(viewParams.dateDisplayStart, "days"); const offsetStart = date.diff(viewParams.dateDisplayStart, "days");
element.style.left = calculatePositionValueForDayCount(viewParams, offsetStart); element.style.left = calculatePositionValueForDayCount(viewParams, offsetStart);
return true;
} }
/** /**
@ -85,14 +88,12 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
el.className = timelineElementCssClass + " " + this.type; el.className = timelineElementCssClass + " " + this.type;
el.style.position = "relative"; el.style.position = "relative";
el.style.height = "1em";
el.style.backgroundColor = this.typeColor(renderInfo.workPackage as any);
el.style.borderRadius = "2px"; el.style.borderRadius = "2px";
el.style.zIndex = "50"; el.style.zIndex = "50";
el.style.cursor = "ew-resize"; el.style.cursor = "ew-resize";
el.style.transform = 'rotate(45deg)'; el.style.transform = 'rotate(45deg)';
el.style.transformOrigin = '75% 100%'; el.style.transformOrigin = 'center center';
return el; return el;
} }
} }

@ -64,7 +64,9 @@ export function registerWorkPackageMouseHandler(this: void,
// Let the renderer decide which fields we change // Let the renderer decide which fields we change
renderer.assignDateValues(wp, dates); renderer.assignDateValues(wp, dates);
wpCacheService.updateWorkPackage(wp as any);
// Update the work package to refresh dates columns
wpCacheService.updateWorkPackage(wp);
} }
function mouseMoveFn(ev: JQueryEventObject) { function mouseMoveFn(ev: JQueryEventObject) {
@ -72,7 +74,7 @@ export function registerWorkPackageMouseHandler(this: void,
const distance = Math.floor((mev.clientX - startX) / renderInfo.viewParams.pixelPerDay); const distance = Math.floor((mev.clientX - startX) / renderInfo.viewParams.pixelPerDay);
const days = distance < 0 ? distance + 1 : distance; const days = distance < 0 ? distance + 1 : distance;
dateStates = renderer.onDaysMoved(dateStates, days); dateStates = renderer.onDaysMoved(renderInfo.workPackage, days);
applyDateValues(dateStates); applyDateValues(dateStates);
} }
@ -106,6 +108,9 @@ export function registerWorkPackageMouseHandler(this: void,
if (cancelled) { if (cancelled) {
renderer.onCancel(renderInfo.workPackage, dateStates); renderer.onCancel(renderInfo.workPackage, dateStates);
} else {
// Persist the changes
wpCacheService.saveIfChanged(renderInfo.workPackage);
} }
jBody.off("mousemove", mouseMoveFn); jBody.off("mousemove", mouseMoveFn);
@ -119,6 +124,7 @@ export function registerWorkPackageMouseHandler(this: void,
dateStates = {}; dateStates = {};
workPackageTimeline.refreshView(); workPackageTimeline.refreshView();
} }
} }

@ -76,14 +76,21 @@ export class WorkPackageTimelineCell {
} }
private lazyInit(renderer: TimelineCellRenderer, renderInfo: RenderInfo) { 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 already rendered with correct shape, ignore
if (this.element !== null && (this.elementShape === renderer.type)) { if (wasRendered && (this.elementShape === renderer.type)) {
return; return;
} }
// Remove the element first if we're redrawing // Remove the element first if we're redrawing
if (this.element !== null) { if (wasRendered) {
this.element.parentNode.removeChild(this.element); this.element.parentNode.removeChild(this.element);
} }

@ -91,17 +91,11 @@ export class WpTimelineHeader {
this.scrollBar = this.outerHeader.find('.wp-timeline--slider'); this.scrollBar = this.outerHeader.find('.wp-timeline--slider');
this.scrollBar.slider({ this.scrollBar.slider({
min: 0, min: 0,
max: 50, // TODO change to actual max
slide: (evt, ui) => { slide: (evt, ui) => {
this.wpTimeline.viewParameterSettings.scrollOffsetInDays = -ui.value; this.wpTimeline.viewParameterSettings.scrollOffsetInDays = -ui.value;
this.wpTimeline.refreshScrollOnly(); this.wpTimeline.refreshScrollOnly();
// The handle uses left offset to set the current position. this.recalculateScrollBarLeftMargin(ui.value);
// 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);
} }
}); });
@ -109,11 +103,28 @@ export class WpTimelineHeader {
} }
private updateScrollbar(vp: TimelineViewParameters) { private updateScrollbar(vp: TimelineViewParameters) {
this.scrollBar.slider('option', 'max', vp.maxSteps); let maxWidth = this.scrollBar.width(),
daysDisplayed = Math.min(vp.maxSteps, Math.floor(maxWidth / vp.pixelPerDay)),
// Set width of handle newMax = vp.maxSteps - daysDisplayed,
let newWidth = Math.max((vp.maxSteps / vp.pixelPerDay), 20) + 'px'; 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.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() { private lazyInit() {

Loading…
Cancel
Save