File

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

Implements

OnInit OnDestroy

Metadata

selector app-issue-detail
styleUrls ./issue-detail.component.scss
templateUrl ./issue-detail.component.html

Index

Properties
Methods

Constructor

constructor(dataService: DataService, activatedRoute: ActivatedRoute)
Parameters :
Name Type Optional
dataService DataService No
activatedRoute ActivatedRoute No

Methods

beginEditing
beginEditing()

Begins the editing process in which:

  1. the issue title and
  2. the issue category can be changed.
Returns : void
Public finishEditing
finishEditing(save?: boolean)

Finishes the editing process in which:

  1. the issue title and
  2. the issue category can be changed.
Parameters :
Name Type Optional Description
save boolean Yes
  • Boolean that indicates whether to save the new title.
Returns : void
formatIssueOpenTime
formatIssueOpenTime()
Returns : string
Private saveChanges
saveChanges()

Saves all changes to the current issue.

Returns : void

Properties

Public activatedRoute
Type : ActivatedRoute
category
Default value : new FormControl('', [Validators.required])
inputTitle
Type : ElementRef
Decorators :
@ViewChild('titleInput')
Public issue$
Type : DataNode<Issue>
Public issueEditable
Default value : false
Public issueId
Type : string
Public issueSub
Type : Subscription
Public projectId
Type : string
Public savingChanges
Default value : false
Public timeFormatter
Default value : new TimeFormatter()
Public userCanEditIssue
Default value : true
import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Issue} from 'src/generated/graphql-dgql';
import {Subscription} from 'rxjs';
import {NodeType} from '@app/data-dgql/id';
import {DataNode} from '@app/data-dgql/query';
import DataService from '@app/data-dgql';
import {TimeFormatter} from '@app/issue-detail/time-formatter';
import {FormControl, Validators} from '@angular/forms';

@Component({
  selector: 'app-issue-detail',
  templateUrl: './issue-detail.component.html',
  styleUrls: ['./issue-detail.component.scss']
})
/**
 * This component provides detailed information about an issue.
 * It also lets the user edit properties of an issue.
 */
export class IssueDetailComponent implements OnInit, OnDestroy {
  // current project id
  public projectId: string;

  // current issue id
  public issueId: string;

  // mark whether the current issue is editable
  public issueEditable = false;

  // mark whether changes to the current issue are being saved,
  // used for the loading spinner of the Save button
  public savingChanges = false;

  // provides functions for time formatting
  public timeFormatter = new TimeFormatter();

  // FIXME: replace with issue$.current?.userCanEditIssue in HTML once that works
  public userCanEditIssue = true;

  public issue$: DataNode<Issue>;
  public issueSub: Subscription;

  // new title of the current issue
  @ViewChild('titleInput') inputTitle: ElementRef;

  // new category of the current issue
  category = new FormControl('', [Validators.required]);

  constructor(private dataService: DataService, public activatedRoute: ActivatedRoute) {}

  ngOnInit(): void {
    this.projectId = this.activatedRoute.snapshot.paramMap.get('id');
    this.issueId = this.activatedRoute.snapshot.paramMap.get('issueId');
    const issueNodeId = {type: NodeType.Issue, id: this.issueId};

    this.issue$ = this.dataService.getNode(issueNodeId);
    this.issueSub = this.issue$.subscribe();
  }

  ngOnDestroy() {
    this.issueSub.unsubscribe();
  }

  formatIssueOpenTime(): string {
    if (this.issue$.hasData) {
      return this.timeFormatter.formatTimeDifference(this.issue$.current.createdAt);
    }
  }

  /**
   * Begins the editing process in which:
   * 1) the issue title and
   * 2) the issue category can be changed.
   */
  beginEditing(): void {
    // marks the issue as editable
    this.issueEditable = true;

    // sets up the issue category
    this.issue$.dataAsPromise().then((data) => {
      this.category.setValue(data.category);
    });
  }

  /**
   * Finishes the editing process in which:
   * 1) the issue title and
   * 2) the issue category can be changed.
   * @param save - Boolean that indicates whether to save the new title.
   */
  public finishEditing(save?: boolean): void {
    // case: the new changes are to be saved
    if (save) {
      // marks the saving process as started
      this.savingChanges = true;

      // saves all changes
      this.saveChanges();
    }

    // case: the new changes are not to be saved
    else {
      this.issueEditable = false;
    }
  }

  /**
   * Saves all changes to the current issue.
   */
  private saveChanges() {
    // 1) saves the new title
    this.dataService.mutations.renameIssueTitle(Math.random().toString(), this.issue$.id, this.inputTitle.nativeElement.value);

    // 2) saves the new category
    this.dataService.mutations
      .changeIssueCategory(Math.random().toString(), this.issue$.id, this.category.value)
      .then(() => {
        // marks the issue as uneditable
        this.issueEditable = false;
      })
      .finally(() => {
        // marks the saving process as finished
        this.savingChanges = false;
      });
  }
}
<div class="issue-details" *ngIf="issue$?.current as issue">
  <!-- Issue header-->
  <header class="issue-header">
    <div class="issue-title">
      <!-- Issue title -->
      <h1 class="title-text" *ngIf="!this.issueEditable">{{ issue.title }}</h1>

      <!-- Issue title input field (in edit mode) -->
      <mat-form-field class="title-input" *ngIf="this.issueEditable" appearance="outline">
        <input matInput #titleInput class="title-input" [value]="issue.title" autofocus />
      </mat-form-field>

      <div class="title-actions" *ngIf="userCanEditIssue">
        <!--Edit button-->
        <button mat-flat-button class="title-edit-button" color="primary" (click)="beginEditing()" *ngIf="!this.issueEditable">Edit</button>

        <ng-container *ngIf="this.issueEditable">
          <!-- Cancel button (in edit mode) -->
          <button mat-flat-button (click)="this.finishEditing()">Cancel</button>

          &nbsp;

          <!-- Save button (in edit mode) -->
          <button mat-flat-button (click)="this.finishEditing(true)" [class.spinner]="savingChanges" color="accent">Save</button>
        </ng-container>
      </div>
    </div>

    <div class="issue-category">
      <!-- Issue category buttons (in edit mode) -->
      <mat-button-toggle-group class="category-selector" *ngIf="this.issueEditable" [formControl]="category" required>
        <mat-button-toggle value="UNCLASSIFIED">
          <mat-icon [svgIcon]="'issue-uncategorized'"></mat-icon>
          Unclassified
        </mat-button-toggle>
        <mat-button-toggle value="BUG">
          <mat-icon [svgIcon]="'issue-bug'"></mat-icon>
          Bug
        </mat-button-toggle>
        <mat-button-toggle value="FEATURE_REQUEST">
          <mat-icon [svgIcon]="'issue-feature'"></mat-icon>
          Feature Request
        </mat-button-toggle>
      </mat-button-toggle-group>
    </div>

    <div class="issue-subheading">
      <!-- Issue category and status -->
      <div [ngClass]="['issue-status', issue.isOpen ? 'is-open' : 'is-closed']">
        <app-issue-icon class="status-icon" [issue]="issue"></app-issue-icon>
        <span class="status-label">
          <ng-container *ngIf="issue.isOpen">Open</ng-container>
          <ng-container *ngIf="!issue.isOpen">Closed</ng-container>
        </span>
      </div>

      <!-- Issue info about the user and date -->
      <div class="issue-opened">
        <app-user-item [noDisplay]="true" [user]="issue.createdBy"></app-user-item>
        opened this issue
        <time [dateTime]="issue.createdAt" [title]="this.timeFormatter.formatTime(issue.createdAt)">
          {{ this.formatIssueOpenTime() }}
        </time>
      </div>

      <!-- Issue comment count -->
      <div class="issue-stats">&nbsp;&sdot;&nbsp;{{ this.timeFormatter.pluralize(issue.issueComments?.totalCount + 1, "comment") }}</div>
    </div>
  </header>

  <div class="issue-page">
    <app-issue-contents class="issue-contents" [issue$]="issue$" [projectId]="projectId"></app-issue-contents>
    <app-issue-sidebar class="issue-sidebar" [issue$]="issue$" [issueId]="issueId" [projectId]="projectId"></app-issue-sidebar>
  </div>
</div>

./issue-detail.component.scss

@import "src/styles/variables.scss";
@import "src/styles/spinner";

.category-selector {
  width: 100%;
  margin-bottom: 8px;

  mat-button-toggle {
    width: 100%;

    ::ng-deep &.mat-button-toggle-checked {
      font-weight: 600;
    }
  }
}

.issue-details {
  padding: 16px;

  .issue-header {
    padding-bottom: 16px;
    border-bottom: 1px solid rgba(0, 0, 0, 0.15);
    margin-bottom: 16px;
  }

  .issue-page {
    display: flex;

    .issue-contents {
      flex: 2;
    }

    .issue-sidebar {
      flex: 1;
      max-width: 300px;
      margin-left: 16px;
    }
  }
}

@media (max-width: 600px) {
  // put issue sidebar below on narrow displays
  .issue-details .issue-page {
    display: block;
  }
}

.issue-header {
  .issue-title {
    display: flex;
    align-items: center;
    margin-bottom: 8px;

    .title-text {
      margin: 0;
      margin-right: 16px;
      flex: 1;
    }

    .title-input {
      flex: 1;
      margin-right: 16px;

      ::ng-deep .mat-form-field-wrapper {
        padding-bottom: 0;

        .mat-form-field-infix {
          border-top-width: 0.5em;
        }
      }
    }
  }

  .issue-category {
    display: flex;
    align-items: center;
    margin-bottom: 8px;
  }

  .issue-subheading {
    display: flex;
    align-items: center;

    .issue-status {
      display: inline-flex;
      padding: 2px 8px 2px 4px;
      border-radius: 100px;
      border: 1px solid rgba(0, 0, 0, 0.1);
      align-items: center;
      font-weight: 500;
      margin-right: 8px;

      .status-icon {
        margin-right: 2px;
      }
      .status-label {
        display: block;
      }

      .status-icon ::ng-deep svg {
        .state[fill] {
          fill: white;
        }
        .state[stroke] {
          stroke: white;
        }
        .arrow-in,
        .arrow-out,
        .assigned-star {
          fill: rgba(255, 255, 255, 0.7);
        }
      }

      &.is-open {
        color: white;
        background: #00ba39;
      }
      &.is-closed {
        color: white;
        background: #ff0036;
      }
    }

    .issue-opened,
    .issue-stats {
      display: inline-block;
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""