File

src/app/issue-detail/timeline/timeline.component.ts

Description

This interface may contain in contrast to a normal timeline item several events in one item. Timeline items are coalesced because they are performed in close succession. label 1 and label 2 are added to an issue at the same time. Instead of representing them as to individual items/events, they will be represented as one item in the timeline.

Index

Properties

Properties

isCoalesced
isCoalesced: boolean
Type : boolean
item
item: Array<IssueTimelineItem> | IssueTimelineItem
Type : Array<IssueTimelineItem> | IssueTimelineItem
time
time: string
Type : string
type
type: string
Type : string
user
user: string
Type : string
import {AfterViewInit, Component, Input, ViewChild} from '@angular/core';
import {TimeFormatter} from '@app/issue-detail/time-formatter';
import {IssueTimelineItem} from '../../../generated/graphql-dgql';
import {DataList} from '@app/data-dgql/query';
import DataService from '@app/data-dgql';
import {ListType, NodeId, NodeType} from '@app/data-dgql/id';
import {QueryComponent} from '@app/utils/query-component/query.component';

/**
 * This interface may contain in contrast to a normal timeline item several events in one item.
 * Timeline items are coalesced because they are performed in close succession.
 * @example
 * label 1 and label 2 are added to an issue at the same time.
 * Instead of representing them as to individual items/events, they will be represented as one item in the timeline.
 */
export interface CoalescedTimelineItem {
  user: string;
  type: string;
  isCoalesced: boolean;
  item: Array<IssueTimelineItem> | IssueTimelineItem;
  time: string;
}

type ItemFilterFunction = (IssueTimelineItem) => boolean;

/**
 * This component shows the full timeline
 * with all its timeline events for a given issue.
 */
@Component({
  selector: 'app-timeline',
  templateUrl: './timeline.component.html',
  styleUrls: ['./timeline.component.scss']
})
export class TimelineComponent implements AfterViewInit {
  /**
   * Events which need to be coalesced
   */
  static readonly COALESCABLE_EVENTS: Map<string, ItemFilterFunction> = new Map([
    [
      'LabelledEvent',
      (item) => {
        return !!item.label;
      }
    ],
    [
      'UnlabelledEvent',
      (item) => {
        return !!item.removedLabel;
      }
    ],
    [
      'AddedToComponentEvent',
      (item) => {
        return !!item.component;
      }
    ],
    [
      'RemovedFromComponentEvent',
      (item) => {
        return !!item.removedComponent;
      }
    ],
    [
      'AddedToLocationEvent',
      (item) => {
        return !!item.location;
      }
    ],
    [
      'RemovedFromLocationEvent',
      (item) => {
        return !!item.removedLocation;
      }
    ],
    [
      'LinkEvent',
      (item) => {
        return !!item.linkedIssue;
      }
    ],
    [
      'UnlinkEvent',
      (item) => {
        return !!item.removedLinkedIssue;
      }
    ]
  ]);

  /**
   * provides functionality for time formatting for correct representation
   */
  public timeFormatter = new TimeFormatter();
  /**
   * Already coalesced items for timeline representation
   */
  timelineItems: Array<CoalescedTimelineItem> = [];
  /**
   * Subscription for timelineitems
   */
  public timelineItems$: DataList<IssueTimelineItem, unknown>;
  /**
   * The id of the corresponding issue for which the timeline is shown
   */
  @Input() issueId: NodeId;
  /**
   * The id of the project in which the issue is listed
   */
  @Input() projectID: string;
  /**
   * Component which is handling the query to the server
   */
  @ViewChild(QueryComponent) query: QueryComponent;

  /**
   * Check if a timeline item can be coalesced with another timeline item. This is the case if
   * a) the user is the same and
   * b) both items were created within the span of a minute
   * @param previousItem The previous item
   * @param nextItem The item to be coalesced with the previous item
   * @returns True if both items can be coalesced
   */
  private static shouldStopCoalescing(previousItem: IssueTimelineItem, nextItem: IssueTimelineItem): boolean {
    return (
      previousItem.createdBy.id !== previousItem.createdBy.id ||
      Math.abs(Date.parse(nextItem.createdAt) - Date.parse(nextItem.createdAt)) > 60000
    );
  }

  /**
   * Service for handling API connection is required
   * @param dataService handling api connection
   */
  constructor(private dataService: DataService) {}

  ngAfterViewInit(): void {
    this.requestTimelineItems();
  }

  /**
   * Retrieves all timeline items (aka. timeline events) for the current issue.
   * Use in ngAfterViewInit() lifecycle hook
   */
  requestTimelineItems(): void {
    // gets an observable with all timeline items for thecurrent issue
    this.timelineItems$ = this.dataService.getList({
      node: this.issueId,
      type: ListType.TimelineItems
    });

    // FIXME: decide on the count
    this.timelineItems$.count = 99999;

    this.query.listenTo(this.timelineItems$, (value) => {
      this.prepareTimelineItems(value);
    });
  }

  /**
   * Prepares the timeline items (aka. the timeline events).
   * @param items Timeline items to prepare.
   */
  prepareTimelineItems(items: Map<string, IssueTimelineItem>): void {
    let coalescingType: string = null;
    let coalesceList = new Array<IssueTimelineItem>();
    const coalesced: Array<CoalescedTimelineItem> = [];

    for (const timelineItem of items.values()) {
      const itemType: string = (timelineItem as any).__typename;
      const filter = TimelineComponent.COALESCABLE_EVENTS.get(itemType);
      let stopCoalescing = false;

      // decides whether to stop coalescing
      if (coalescingType) {
        stopCoalescing = TimelineComponent.shouldStopCoalescing(coalesceList[0], timelineItem);
      }

      // case: the coalescing type equals the current item type
      // or coalescing should stop
      if (coalescingType !== itemType || stopCoalescing) {
        // adds remaining items
        this.finishCoalescing(coalesceList, coalesced);
        coalesceList = [];

        if (filter && filter(timelineItem)) {
          coalescingType = itemType;
          coalesceList.push(timelineItem);
        } else {
          coalescingType = null;
          this.addSingleCoalesceItem(timelineItem, filter, coalesced);
        }

        continue;
      } else if (coalescingType === null) {
        this.addSingleCoalesceItem(timelineItem, filter, coalesced);
        continue;
      }

      if (filter(timelineItem)) {
        coalesceList.push(timelineItem);
      }
    }

    // adds remaining items
    this.finishCoalescing(coalesceList, coalesced);
    this.timelineItems = coalesced;
  }

  /**
   * Returns the name of the user
   * that created a given timeline item (aka. timeline event)
   * or just "Deleted User" in case the user no longer exists.
   * @param item The given timeline item.
   * @returns Name of the timeline item creator.
   */
  private userName(item: IssueTimelineItem) {
    // case: the timeline item's creator's name can be retrieved
    if (item.createdBy) {
      return item.createdBy.displayName;
    }

    return 'Deleted User';
  }

  /**
   * Turns a list of timeline items into a coalesced timeline item, and adds them to a list of coalesced timeline items.
   *
   * @param coalesceList The list of timeline items
   * @param coalesced The list of all coalesced timeline items
   */
  private finishCoalescing(coalesceList: IssueTimelineItem[], coalesced: CoalescedTimelineItem[]): void {
    if (coalesceList.length === 0) {
      return;
    }

    const firstItem: any = coalesceList[0];
    const itemType = firstItem.__typename;
    const createdBy = this.userName(firstItem);

    if (coalesceList.length > 1) {
      // Combine multiple timeline items into one
      coalesced.push({
        type: itemType,
        isCoalesced: true,
        item: coalesceList,
        user: createdBy,
        time: coalesceList[0].createdAt
      });
    } else if (coalesceList.length === 1) {
      // Wrap a single timeline item into a coalesced timeline item
      coalesced.push({
        type: itemType,
        isCoalesced: false,
        item: coalesceList[0],
        user: createdBy,
        time: coalesceList[0].createdAt
      });
    }
  }

  /**
   * Adds a single item to a list containing all coalesced timeline items.
   * @param timelineItem The given item.
   * @param filter Filter used on the given item.
   * @param coalesced The list of coalesced timeline items
   */
  private addSingleCoalesceItem(
    timelineItem: IssueTimelineItem,
    filter: ItemFilterFunction | undefined,
    coalesced: CoalescedTimelineItem[]
  ): void {
    if (!filter || filter(timelineItem)) {
      coalesced.push({
        type: (timelineItem as any).__typename,
        isCoalesced: false,
        item: timelineItem,
        user: this.userName(timelineItem),
        time: timelineItem.createdAt
      });
    }
  }

  /**
   * Handles the id for a given node...
   */
  makeCommentId(node): NodeId {
    return {type: NodeType.IssueComment, id: node.id};
  }
}

results matching ""

    No results matching ""