File

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

Description

This component shows the full timeline with all its timeline events for a given issue.

Implements

AfterViewInit

Metadata

selector app-timeline
styleUrls ./timeline.component.scss
templateUrl ./timeline.component.html

Index

Properties
Methods
Inputs

Constructor

constructor(dataService: DataService)

Service for handling API connection is required

Parameters :
Name Type Optional Description
dataService DataService No

handling api connection

Inputs

issueId
Type : NodeId

The id of the corresponding issue for which the timeline is shown

projectID
Type : string

The id of the project in which the issue is listed

Methods

Private addSingleCoalesceItem
addSingleCoalesceItem(timelineItem: IssueTimelineItem, filter: ItemFilterFunction | undefined, coalesced: CoalescedTimelineItem[])

Adds a single item to a list containing all coalesced timeline items.

Parameters :
Name Type Optional Description
timelineItem IssueTimelineItem No

The given item.

filter ItemFilterFunction | undefined No

Filter used on the given item.

coalesced CoalescedTimelineItem[] No

The list of coalesced timeline items

Returns : void
Private finishCoalescing
finishCoalescing(coalesceList: IssueTimelineItem[], coalesced: CoalescedTimelineItem[])

Turns a list of timeline items into a coalesced timeline item, and adds them to a list of coalesced timeline items.

Parameters :
Name Type Optional Description
coalesceList IssueTimelineItem[] No

The list of timeline items

coalesced CoalescedTimelineItem[] No

The list of all coalesced timeline items

Returns : void
makeCommentId
makeCommentId(node)

Handles the id for a given node...

Parameters :
Name Optional
node No
Returns : NodeId
prepareTimelineItems
prepareTimelineItems(items: Map)

Prepares the timeline items (aka. the timeline events).

Parameters :
Name Type Optional Description
items Map<string | IssueTimelineItem> No

Timeline items to prepare.

Returns : void
requestTimelineItems
requestTimelineItems()

Retrieves all timeline items (aka. timeline events) for the current issue. Use in ngAfterViewInit() lifecycle hook

Returns : void
Private Static shouldStopCoalescing
shouldStopCoalescing(previousItem: IssueTimelineItem, nextItem: IssueTimelineItem)

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

Parameters :
Name Type Optional Description
previousItem IssueTimelineItem No

The previous item

nextItem IssueTimelineItem No

The item to be coalesced with the previous item

Returns : boolean

True if both items can be coalesced

Private userName
userName(item: IssueTimelineItem)

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.

Parameters :
Name Type Optional Description
item IssueTimelineItem No

The given timeline item.

Returns : any

Name of the timeline item creator.

Properties

Static Readonly COALESCABLE_EVENTS
Type : Map<string | ItemFilterFunction>
Default value : 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; } ] ])

Events which need to be coalesced

query
Type : QueryComponent
Decorators :
@ViewChild(QueryComponent)

Component which is handling the query to the server

Public timeFormatter
Default value : new TimeFormatter()

provides functionality for time formatting for correct representation

timelineItems
Type : Array<CoalescedTimelineItem>
Default value : []

Already coalesced items for timeline representation

Public timelineItems$
Type : DataList<IssueTimelineItem | >

Subscription for timelineitems

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};
  }
}
<!--Timeline of a given issue showing all its events-->
<app-query-component errorMessage="Failed to load timeline items">
  <ng-template appQueryBody>
    <div class="container">
      <div class="page-header"></div>

      <ul class="timeline">
        <li *ngFor="let timelineItem of timelineItems" [ngSwitch]="timelineItem.type" class="timeline-inverted">
          <!--Icons used in the timeline-->
          <div class="timeline-badge" *ngSwitchCase="'AddedToComponentEvent'">
            <mat-icon>add</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'RemovedFromComponentEvent'">
            <mat-icon>remove</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'AddedToLocationEvent'">
            <mat-icon>add</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'RemovedFromLocationEvent'">
            <mat-icon>remove</mat-icon>
          </div>
          <div class="timeline-badge closed" *ngSwitchCase="'ClosedEvent'">
            <mat-icon>task_alt</mat-icon>
          </div>
          <div class="timeline-badge reopened" *ngSwitchCase="'ReopenedEvent'">
            <mat-icon>add_task</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'AssignedEvent'">
            <mat-icon>person_add</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'UnassignedEvent'">
            <mat-icon>person_remove</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'CategoryChangedEvent'">
            <mat-icon>category</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'DueDateChangedEvent'">
            <mat-icon>event</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'EstimatedTimeChangedEvent'">
            <mat-icon>date_range</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'LabelledEvent'">
            <mat-icon>new_label</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'UnlabelledEvent'">
            <mat-icon>label</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'DeletedIssueComment'">
            <mat-icon>speaker_notes_off</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'MarkedAsDuplicateEvent'">
            <mat-icon>plagiarism</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'UnmarkedAsDuplicateEvent'">
            <mat-icon>file_copy</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'LinkEvent'">
            <mat-icon>add_link</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'UnlinkEvent'">
            <mat-icon>link_off</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'PinnedEvent'">
            <mat-icon>push_pin</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'UnpinnedEvent'">
            <mat-icon>push_pin</mat-icon>
          </div>
          <div class="timeline-badge warning" *ngSwitchCase="'ReferencedByIssueEvent'">
            <mat-icon>switch_access_shortcut_add</mat-icon>
          </div>
          <div class="timeline-badge warning" *ngSwitchCase="'ReferencedByOtherEvent'">
            <mat-icon>switch_access_shortcut</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'StartDateChangedEvent'">
            <mat-icon>event</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'PriorityChangedEvent'">
            <mat-icon>reorder</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'RenamedTitleEvent'">
            <mat-icon>drive_file_rename_outline</mat-icon>
          </div>
          <div class="timeline-badge warning" *ngSwitchCase="'WasLinkedEvent'">
            <mat-icon>add_link</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'WasUnlinkedEvent'">
            <mat-icon>link_off</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'AddedArtifactEvent'">
            <mat-icon>data_object</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'RemovedArtifactEvent'">
            <mat-icon>data_object</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'AddedNonFunctionalConstraintEvent'">
            <mat-icon>border_style</mat-icon>
          </div>
          <div class="timeline-badge" *ngSwitchCase="'RemovedNonFunctionalConstraintEvent'">
            <mat-icon>border_style</mat-icon>
          </div>

          <!--Timeline events-->

          <div class="commentContainer" *ngSwitchCase="'IssueComment'">
            <app-comment [commentId]="makeCommentId(timelineItem.item)" [issueId]="issueId" [isIssueBody]="false"></app-comment>
          </div>

          <app-timeline-event-closed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'ClosedEvent'">
          </app-timeline-event-closed>

          <app-timeline-event-assigned class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'AssignedEvent'">
          </app-timeline-event-assigned>

          <app-timeline-event-due-date-changed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'DueDateChangedEvent'">
          </app-timeline-event-due-date-changed>

          <app-timeline-event-estimated-time-changed
            class="fill-width"
            [timelineItem]="timelineItem"
            *ngSwitchCase="'EstimatedTimeChangedEvent'"
          >
          </app-timeline-event-estimated-time-changed>

          <app-timeline-event-labelled class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'LabelledEvent'">
          </app-timeline-event-labelled>

          <app-timeline-event-deleted-issue-comment class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'DeletedIssueComment'">
          </app-timeline-event-deleted-issue-comment>

          <app-timeline-event-marked-duplicate
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'MarkedAsDuplicateEvent'"
          >
          </app-timeline-event-marked-duplicate>

          <app-timeline-event-link class="fill-width" [timelineItem]="timelineItem" [projectID]="projectID" *ngSwitchCase="'LinkEvent'">
          </app-timeline-event-link>

          <app-timeline-event-pinned class="fill-width" [timelineItem]="timelineItem" [projectID]="projectID" *ngSwitchCase="'PinnedEvent'">
          </app-timeline-event-pinned>

          <app-timeline-event-referenced-by-issue
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'ReferencedByIssueEvent'"
          >
          </app-timeline-event-referenced-by-issue>

          <app-timeline-event-priority-changed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'PriorityChangedEvent'">
          </app-timeline-event-priority-changed>

          <app-timeline-event-removed-from-component
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'RemovedFromComponentEvent'"
          >
          </app-timeline-event-removed-from-component>

          <app-timeline-event-referenced-by-other
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'ReferencedByOtherEvent'"
          >
          </app-timeline-event-referenced-by-other>

          <app-timeline-event-removed-from-location
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'RemovedFromLocationEvent'"
          >
          </app-timeline-event-removed-from-location>

          <app-timeline-event-reopened class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'ReopenedEvent'">
          </app-timeline-event-reopened>

          <app-timeline-event-start-date-changed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'StartDateChangedEvent'">
          </app-timeline-event-start-date-changed>

          <app-timeline-event-renamed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'RenamedTitleEvent'">
          </app-timeline-event-renamed>

          <app-timeline-event-unassigned class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'UnassignedEvent'">
          </app-timeline-event-unassigned>

          <app-timeline-event-unlabelled class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'UnlabelledEvent'">
          </app-timeline-event-unlabelled>

          <app-timeline-event-was-linked
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'WasLinkedEvent'"
          >
          </app-timeline-event-was-linked>

          <app-timeline-event-unmarked-duplicate
            class="fill-width"
            [timelineItem]="timelineItem"
            *ngSwitchCase="'UnmarkedAsDuplicateEvent'"
          >
          </app-timeline-event-unmarked-duplicate>

          <app-timeline-event-was-unlinked
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'WasUnlinkedEvent'"
          >
          </app-timeline-event-was-unlinked>

          <app-timeline-event-unpinned
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'UnpinnedEvent'"
          >
          </app-timeline-event-unpinned>

          <app-timeline-event-unlink class="fill-width" [timelineItem]="timelineItem" [projectID]="projectID" *ngSwitchCase="'UnlinkEvent'">
          </app-timeline-event-unlink>

          <app-timeline-event-added-artifact class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'AddedArtifactEvent'">
          </app-timeline-event-added-artifact>

          <app-timeline-event-removed-artifact class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'RemovedArtifactEvent'">
          </app-timeline-event-removed-artifact>

          <app-timeline-event-added-nfc
            class="fill-width"
            [timelineItem]="timelineItem"
            *ngSwitchCase="'AddedNonFunctionalConstraintEvent'"
          >
          </app-timeline-event-added-nfc>

          <app-timeline-event-removed-nfc
            class="fill-width"
            [timelineItem]="timelineItem"
            *ngSwitchCase="'RemovedNonFunctionalConstraintEvent'"
          >
          </app-timeline-event-removed-nfc>

          <app-timeline-event-added-to-component
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'AddedToComponentEvent'"
          >
          </app-timeline-event-added-to-component>

          <app-timeline-event-category-changed class="fill-width" [timelineItem]="timelineItem" *ngSwitchCase="'CategoryChangedEvent'">
          </app-timeline-event-category-changed>

          <app-timeline-event-added-to-location
            class="fill-width"
            [timelineItem]="timelineItem"
            [projectID]="projectID"
            *ngSwitchCase="'AddedToLocationEvent'"
          >
          </app-timeline-event-added-to-location>
        </li>
      </ul>
    </div>
  </ng-template>
</app-query-component>

./timeline.component.scss

.timeline {
  list-style: none;
  margin: 0;
  padding: 20px 0 20px;
  position: relative;

  &::before {
    top: 0;
    bottom: 0;
    position: absolute;
    content: " ";
    width: 2px;
    background: rgba(0, 0, 0, 0.12);
    left: 35px;
  }

  & > li {
    margin-bottom: 20px;
    position: relative;
    display: flex;
    align-items: flex-start;

    & > .timeline-badge {
      color: #fff;
      $size: 50px;
      width: $size;
      height: $size;
      line-height: 50px;
      font-size: 2.5em;
      text-align: center;
      position: relative;
      top: 16px;
      margin-left: calc(36px - (#{$size} / 2));
      margin-right: 12px;
      background-color: #999999;
      z-index: 100;
      border-radius: 50%;
    }

    & > .commentContainer {
      flex: 1;
      left: -36px;
      z-index: 100;

      ::ng-deep .timeline-item::before {
        display: none; // hide duplicate line
      }
    }
  }
}
.timeline-badge.warning {
  background-color: #f0ad4e !important;
}

.timeline-badge.closed {
  background-color: #ff0036 !important;
}

.timeline-badge.reopened {
  background-color: #00ba39 !important;
}

.timeline-title {
  margin-top: 0;
  color: inherit;
}

.fill-width {
  width: 100%;
}

.timeline-body > p,
.timeline-body > ul {
  margin-bottom: 0;
}

.timeline-body > p + p {
  margin-top: 5px;
}

.timeline-panel {
  flex: 1;
  border: 1px solid #d4d4d4;
  border-radius: 2px;
  padding: 20px;
  position: relative;
  -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.175);
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.175);

  &::before {
    position: absolute;
    top: 26px;
    display: inline-block;
    border-top: 15px solid transparent;
    border-left: 15px solid #ccc;
    border-right: 0 solid #ccc;
    border-bottom: 15px solid transparent;
    content: " ";
    border-left-width: 0;
    border-right-width: 15px;
    left: -15px;
    right: auto;
  }

  &::after {
    position: absolute;
    top: 27px;
    display: inline-block;
    border-top: 14px solid transparent;
    border-left: 14px solid #fafafa;
    border-right: 0 solid #fafafa;
    border-bottom: 14px solid transparent;
    content: " ";
    border-left-width: 0;
    border-right-width: 14px;
    left: -14px;
    right: auto;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""