opf/openproject

View on GitHub
frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts

Summary

Maintainability
A
55 mins
Test Coverage
import * as moment from 'moment';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
import {
  calculatePositionValueForDayCountingPx,
  RenderInfo,
  timelineElementCssClass,
} from '../wp-timeline';

import {
  classNameFarRightLabel,
  classNameHideOnHover,
  classNameHoverStyle,
  classNameLeftHoverLabel,
  classNameLeftLabel,
  classNameRightContainer,
  classNameRightHoverLabel,
  classNameRightLabel,
  classNameShowOnHover,
  CellDateMovement,
  LabelPosition,
  TimelineCellRenderer,
  MouseDirection,
} from './timeline-cell-renderer';
import { WorkPackageCellLabels } from './wp-timeline-cell-labels';
import Moment = moment.Moment;

export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
  public get type():string {
    return 'milestone';
  }

  public isEmpty(wp:WorkPackageResource) {
    const date = moment(wp.date as any);
    return _.isNaN(date.valueOf());
  }

  public canMoveDates(wp:WorkPackageResource) {
    const schema = this.schemaCache.of(wp);
    return schema.date.writable && schema.isAttributeEditable('date');
  }

  public displayPlaceholderUnderCursor(ev:MouseEvent, renderInfo:RenderInfo):HTMLElement {
    const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay);

    const placeholder = document.createElement('div');
    placeholder.className = 'timeline-element milestone';
    placeholder.style.pointerEvents = 'none';
    placeholder.style.height = '1em';
    placeholder.style.width = '1em';
    placeholder.style.left = `${days * renderInfo.viewParams.pixelPerDay}px`;

    const diamond = document.createElement('div');
    diamond.className = 'diamond';
    diamond.style.left = '0.5em';
    diamond.style.height = '1em';
    diamond.style.width = '1em';
    placeholder.appendChild(diamond);

    this.applyTypeColor(renderInfo, diamond);

    return placeholder;
  }

  /**
   * Assign changed dates to the work package.
   * For generic work packages, assigns start and finish date .
   *
   */
  public assignDateValues(change:WorkPackageChangeset,
    labels:WorkPackageCellLabels,
    dates:any):void {
    this.assignDate(change, 'date', dates.date);
    this.updateLabels(true, labels, change);
  }

  /**
   * Handle movement by <delta> days of milestone.
   */
  public onDaysMoved(change:WorkPackageChangeset,
    dayUnderCursor:Moment,
    delta:number,
    _direction:MouseDirection):CellDateMovement {
    const initialDate = change.pristineResource.date;
    const dates:CellDateMovement = {};

    if (initialDate) {
      dates.date = moment(initialDate).add(delta, 'days');
    }

    return dates;
  }

  public onMouseDown(ev:MouseEvent,
    dateForCreate:string | null,
    renderInfo:RenderInfo,
    labels:WorkPackageCellLabels):MouseDirection {
    // check for active selection mode
    if (renderInfo.viewParams.activeSelectionMode) {
      renderInfo.viewParams.activeSelectionMode(renderInfo.workPackage);
      ev.preventDefault();
      return 'both'; // irrelevant
    }

    let direction:'both' | 'create' = 'both';
    this.workPackageTimeline.forceCursor('ew-resize');

    if (dateForCreate) {
      renderInfo.change.projectedResource.date = dateForCreate;
      direction = 'create';
      return direction;
    }

    this.updateLabels(true, labels, renderInfo.change);

    return direction;
  }

  public update(element:HTMLDivElement, labels:WorkPackageCellLabels|null, renderInfo:RenderInfo):boolean {
    const { viewParams } = renderInfo;
    const date = moment(renderInfo.change.projectedResource.date);

    // abort if no date
    if (_.isNaN(date.valueOf())) {
      return false;
    }

    const diamond = jQuery('.diamond', element)[0];

    diamond.style.width = `${15}px`;
    diamond.style.height = `${15}px`;
    diamond.style.width = `${15}px`;
    diamond.style.height = `${15}px`;
    diamond.style.marginLeft = `${-(15 / 2) + (renderInfo.viewParams.pixelPerDay / 2)}px`;
    this.applyTypeColor(renderInfo, diamond);

    // offset left
    const offsetStart = date.diff(viewParams.dateDisplayStart, 'days');
    element.style.left = `${calculatePositionValueForDayCountingPx(viewParams, offsetStart)}px`;

    // Update labels if any
    if (labels) {
      this.updateLabels(false, labels, renderInfo.change);
    }

    this.checkForActiveSelectionMode(renderInfo, diamond);

    return true;
  }

  getMarginLeftOfLeftSide(renderInfo:RenderInfo):number {
    const { change } = renderInfo;
    const start = moment(change.projectedResource.date);
    const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, 'days');
    return calculatePositionValueForDayCountingPx(renderInfo.viewParams, offsetStart);
  }

  getMarginLeftOfRightSide(ri:RenderInfo):number {
    return this.getMarginLeftOfLeftSide(ri) + ri.viewParams.pixelPerDay;
  }

  getPaddingLeftForIncomingRelationLines(renderInfo:RenderInfo):number {
    return (renderInfo.viewParams.pixelPerDay / 2) - 1;
  }

  getPaddingRightForOutgoingRelationLines(renderInfo:RenderInfo):number {
    return (15 / 2);
  }

  /**
   * Render a milestone element, a single day event with no resize, but
   * move functionality.
   */
  public render(renderInfo:RenderInfo):HTMLDivElement {
    const element = document.createElement('div');
    element.className = `${timelineElementCssClass} ${this.type}`;

    const diamond = document.createElement('div');
    diamond.className = 'diamond';
    element.appendChild(diamond);

    return element;
  }

  createAndAddLabels(renderInfo:RenderInfo, element:HTMLElement):WorkPackageCellLabels {
    // create left label
    const labelLeft = document.createElement('div');
    labelLeft.classList.add(classNameLeftLabel, classNameHideOnHover);
    element.appendChild(labelLeft);

    // create right container
    const containerRight = document.createElement('div');
    containerRight.classList.add(classNameRightContainer);
    element.appendChild(containerRight);

    // create right label
    const labelRight = document.createElement('div');
    labelRight.classList.add(classNameRightLabel, classNameHideOnHover);
    containerRight.appendChild(labelRight);

    // create far right label
    const labelFarRight = document.createElement('div');
    labelFarRight.classList.add(classNameFarRightLabel, classNameHideOnHover);
    containerRight.appendChild(labelFarRight);

    // Create right hover label
    const labelHoverRight = document.createElement('div');
    labelHoverRight.classList.add(classNameRightHoverLabel, classNameShowOnHover, classNameHoverStyle);
    element.appendChild(labelHoverRight);

    // Create left hover label
    const labelHoverLeft = document.createElement('div');
    labelHoverLeft.classList.add(classNameLeftHoverLabel, classNameShowOnHover, classNameHoverStyle);
    element.appendChild(labelHoverLeft);

    const labels = new WorkPackageCellLabels(null, labelLeft, labelHoverLeft, labelRight, labelHoverRight, labelFarRight, renderInfo.withAlternativeLabels);
    this.updateLabels(false, labels, renderInfo.change);

    return labels;
  }

  protected renderHoverLabels(labels:WorkPackageCellLabels, change:WorkPackageChangeset) {
    if (labels.withAlternativeLabels) {
      this.renderLabel(change, labels, 'leftHover', 'date');
      this.renderLabel(change, labels, 'rightHover', 'subject');
    } else {
      this.renderLabel(change, labels, 'rightHover', 'date');
    }
  }

  protected updateLabels(activeDragNDrop:boolean,
    labels:WorkPackageCellLabels,
    change:WorkPackageChangeset) {
    const labelConfiguration = this.wpTableTimeline.getNormalizedLabels(change.projectedResource);

    if (!activeDragNDrop) {
      // normal display

      if (labels.withAlternativeLabels) {
        this.renderLabel(change, labels, 'right', 'subject');
      } else {
        // Show only one date field if left=start, right=dueDate
        if (labelConfiguration.left === 'startDate' && labelConfiguration.right === 'dueDate') {
          this.renderLabel(change, labels, 'left', null);
          this.renderLabel(change, labels, 'right', 'date');
        } else {
          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, change);
  }

  protected renderLabel(change:WorkPackageChangeset,
    labels:WorkPackageCellLabels,
    position:LabelPosition|'leftHover'|'rightHover',
    attribute:string|null) {
    // Normalize attribute
    if (attribute === 'startDate' || attribute === 'dueDate') {
      attribute = 'date';
    }

    super.renderLabel(change, labels, position, attribute);
  }
}