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 {
/**
* it would be better if this was not deduced but rather taken from the type
*/
return this.hasOwnProperty('date');
return this.schema.hasOwnProperty('date');
}
/**

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

@ -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<WorkPackageResource> {
const state = this.states.workPackages.get(getWorkPackageId(workPackageId));
if (forceUpdate) {

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

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

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

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

@ -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() {

Loading…
Cancel
Save