File

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

Description

This component shows all issues for a given component / interface. It lets the user 1) filter through all the issues, 2) create new issues and also 3) sort all issues in a separate table view.

Implements

OnInit AfterViewInit OnDestroy

Metadata

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

Index

Properties
Methods
Inputs

Constructor

constructor(activatedRoute: ActivatedRoute, dialog: MatDialog, router: Router, dataService: DataService)
Parameters :
Name Type Optional
activatedRoute ActivatedRoute No
dialog MatDialog No
router Router No
dataService DataService No

Inputs

listId
Type : ListId
projectId
Type : string

Methods

applyFilter
applyFilter(filter: IssueFilter)

Applies a given filter.

Parameters :
Name Type Optional Description
filter IssueFilter No
  • Given filter to be applied.
Returns : void
clickedOnRow
clickedOnRow(row: any)

Gets activated when an issue is clicked. Navigates the user to the corresponding issue page.

Parameters :
Name Type Optional Description
row any No
  • Issue that is clicked.
Returns : void
formatCategoryDescription
formatCategoryDescription(category: IssueCategory)

Determines issue description depending on the given categiry.

Parameters :
Name Type Optional Description
category IssueCategory No
  • The given issue category.
Returns : string

Issue description.

formatCategoryIcon
formatCategoryIcon(issue: Issue)

Determines issue icon depending on the given category.

Parameters :
Name Type Optional Description
issue Issue No
  • The given issue.
Returns : string

Issue icon id.

Private getQueryParamFilter
getQueryParamFilter()

Gets the query param filter. If it is set, the issue list shows only issues that match the given keyword. Otherwise all issues are displayed.

Returns : string
onAddClick
onAddClick()

Opens a Create Issue dialog. Also selects components and locations depending from which Component / Interface page the Create Issue dialog was initiated. ex. Interface I1 with Prvider Component C1 lead to an Interface Issue with components: Component C1 and locations: Component C1, Interface I1

Returns : void
Private prepareIssueArray
prepareIssueArray()

Prepares the issue array for the filter function. For each issue a search string is defined. The search string contains assignees, labels, and the author. The filter funcion can search inside the string for keywords matching the given search string.

Returns : void

Properties

Public allLabelsList
Type : ListId
Public canCreateNewIssue
Default value : false
columnsToDisplay
Type : []
Default value : ['title', 'author', 'assignees', 'labels', 'category']
Public Optional component$
Type : DataNode<IComponent>
Public Optional componentInterface$
Type : DataNode<ComponentInterface>
Public componentInterfaceProvider
Type : NodeId
Private Optional componentInterfaceSub
Type : Subscription
Private Optional componentSub
Type : Subscription
dataSource
Type : MatTableDataSource<any>
Public Optional list$
Type : DataList<Issue | IssueFilter>
paginator
Type : MatPaginator
Decorators :
@ViewChild(MatPaginator)
query
Type : QueryComponent
Decorators :
@ViewChild(QueryComponent)
Public queryParamFilter
Type : string
Default value : ''
searchIssuesDataArray
Type : any
sort
Type : MatSort
Decorators :
@ViewChild(MatSort)
validationFilter
Default value : new FormControl('')
import {AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {MatTableDataSource} from '@angular/material/table';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort, MatSortable} from '@angular/material/sort';
import {CreateIssueDialogComponent} from '@app/dialogs/create-issue-dialog/create-issue-dialog.component';
import {MatDialog} from '@angular/material/dialog';
import {FormControl} from '@angular/forms';
import DataService from '@app/data-dgql';
import {ListId, ListType, NodeId, NodeType} from '@app/data-dgql/id';
import {DataList, DataNode} from '@app/data-dgql/query';
import {Component as IComponent, ComponentInterface, Issue, IssueCategory, IssueFilter} from '../../generated/graphql-dgql';
import {QueryComponent} from '@app/utils/query-component/query.component';

/**
 * This component shows all issues for a given component / interface.
 * It lets the user 1) filter through all the issues,
 * 2) create new issues and also 3) sort all issues in a separate table view.
 */
@Component({
  selector: 'app-issue-list',
  templateUrl: './issue-list.component.html',
  styleUrls: ['./issue-list.component.scss']
})
export class IssueListComponent implements OnInit, AfterViewInit, OnDestroy {
  @Input() listId: ListId;
  @Input() projectId: string;
  public queryParamFilter = '';
  public list$?: DataList<Issue, IssueFilter>;

  // component that is observed
  public component$?: DataNode<IComponent>;
  private componentSub?: Subscription;

  // interface that is observed
  public componentInterface$?: DataNode<ComponentInterface>;
  private componentInterfaceSub?: Subscription;

  // provider of the interface that is observed
  public componentInterfaceProvider: NodeId;

  // determines whether one can create new issues from a given component / interface page
  // FIXME remove and use proper logic instead
  public canCreateNewIssue = false;

  public allLabelsList: ListId;

  dataSource: MatTableDataSource<any>;
  columnsToDisplay = ['title', 'author', 'assignees', 'labels', 'category'];
  searchIssuesDataArray: any;
  validationFilter = new FormControl('');

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild(QueryComponent) query: QueryComponent;

  constructor(
    private activatedRoute: ActivatedRoute,
    private dialog: MatDialog,
    private router: Router,
    private dataService: DataService
  ) {}

  /**
   * Determines issue icon depending on the given category.
   * @param issue - The given issue.
   * @returns Issue icon id.
   */
  formatCategoryIcon(issue: Issue): string {
    switch (issue.category) {
      case IssueCategory.Bug:
        return issue.isOpen ? 'issue-bug' : 'issue-bug-closed';
      case IssueCategory.FeatureRequest:
        return issue.isOpen ? 'issue-feature' : 'issue-feature-closed';
      case IssueCategory.Unclassified:
        return issue.isOpen ? 'issue-uncategorized' : 'issue-uncategorized-closed';
    }
  }

  /**
   * Determines issue description depending on the given categiry.
   * @param category - The given issue category.
   * @returns Issue description.
   */
  formatCategoryDescription(category: IssueCategory): string {
    switch (category) {
      case IssueCategory.Bug:
        return 'Bug';
      case IssueCategory.FeatureRequest:
        return 'Feature request';
      case IssueCategory.Unclassified:
        return 'Unclassified';
    }
  }

  ngOnInit(): void {
    this.allLabelsList = {
      node: this.listId.node,
      type: ListType.Labels
    };

    if (this.listId.node.type === NodeType.Component) {
      this.canCreateNewIssue = true;
      this.component$ = this.dataService.getNode(this.listId.node);
      this.componentSub = this.component$.subscribe();
    } else if (this.listId.node.type === NodeType.ComponentInterface) {
      this.canCreateNewIssue = true;
      this.componentInterface$ = this.dataService.getNode(this.listId.node);
      this.componentInterfaceSub = this.componentInterface$.subscribe();

      this.componentInterface$.dataAsPromise().then((data) => {
        this.componentInterfaceProvider = {
          type: NodeType.Component,
          id: data.component.id
        };
      });
    }

    // FIXME: a hack to fix the labels list on interfaces
    if (this.listId.node.type === NodeType.ComponentInterface) {
      const interfaceNode = this.dataService.getNode<ComponentInterface>(this.listId.node);
      interfaceNode.dataAsPromise().then((data) => {
        this.allLabelsList = {
          node: {type: NodeType.Component, id: data.component.id},
          type: ListType.Labels
        };
      });
    }

    this.list$ = this.dataService.getList(this.listId);
    this.list$.count = 25;
  }

  ngAfterViewInit() {
    this.query.listenTo(this.list$, (data) => {
      this.dataSource = new MatTableDataSource<any>(data ? [...data.values()] : []);
      this.sort.sort({id: 'category', start: 'asc'} as MatSortable);
      this.dataSource.sort = this.sort;
      // FIXME use bespoke pagination/sorting/filtering
      // this.dataSource.paginator = this.paginator;
      this.dataSource.filter = this.getQueryParamFilter();
      this.validationFilter.setValue(this.getQueryParamFilter());
      this.prepareIssueArray();
    });
  }

  ngOnDestroy() {
    this.componentSub?.unsubscribe();
    this.componentInterfaceSub?.unsubscribe();
  }

  /**
   * Gets the query param filter.
   * If it is set, the issue list shows only issues that match the given keyword.
   * Otherwise all issues are displayed.
   */
  private getQueryParamFilter(): string {
    let returnedFilter = '';
    this.activatedRoute.queryParams.subscribe((params) => {
      // case: query param filter is set
      // => shows only matching issues
      if (params.filter) {
        this.queryParamFilter = params.filter;
        returnedFilter = params.filter;
      }

      // case: query param filter is not set
      // => shows all issues
      else {
        returnedFilter = '';
      }
    });
    return returnedFilter;
  }

  /**
   * Applies a given filter.
   * @param filter - Given filter to be applied.
   */
  applyFilter(filter: IssueFilter): void {
    this.list$.filter = filter;
  }

  /**
   * Gets activated when an issue is clicked.
   * Navigates the user to the corresponding issue page.
   * @param row - Issue that is clicked.
   */
  clickedOnRow(row: any): void {
    this.router.navigate(['/projects', this.projectId, 'issues', row.id]);
  }

  /**
   * Prepares the issue array for the filter function.
   * For each issue a search string is defined.
   * The search string contains assignees, labels, and the author.
   * The filter funcion can search inside the string for keywords matching the given search string.
   */
  private prepareIssueArray() {
    // FIXME use API search
    if (!this.list$.hasData) {
      return;
    }
    this.searchIssuesDataArray = [...this.list$.current.values()];
    for (const issue of this.searchIssuesDataArray) {
      let additionalSearchString = '';
      issue.assigneesString = '';
      issue.labelsString = '';

      // adds all assignees
      for (const assignee of issue.assignees.nodes) {
        additionalSearchString += ' ' + assignee.displayName;
        issue.assigneesString += ' ' + assignee.displayName;
      }

      // adds all labels
      for (const label of issue.labels.nodes) {
        additionalSearchString += ' ' + label.name;
        issue.labelsString += ' ' + label.name;
      }

      // adds the author
      additionalSearchString += ' ' + issue.createdBy.displayName;

      issue.search = additionalSearchString;
    }
  }

  /**
   * Opens a Create Issue dialog.
   * Also selects components and locations depending from which
   * Component / Interface page the Create Issue dialog was initiated.
   * ex. Interface I1 with Prvider Component C1 lead to an Interface Issue
   * with components: Component C1 and locations: Component C1, Interface I1
   */
  onAddClick(): void {
    // FIXME move functionality so that the component can be reusable as a list

    // case: node is a component
    if (this.listId.node.type === NodeType.Component) {
      this.dialog.open(CreateIssueDialogComponent, {
        data: {
          projectId: this.projectId,
          components: [this.component$.id]
        },
        width: '600px'
      });
    }

    // case: node is an interface
    else if (this.listId.node.type === NodeType.ComponentInterface) {
      this.dialog.open(CreateIssueDialogComponent, {
        data: {
          projectId: this.projectId,
          components: [this.componentInterfaceProvider],
          locations: [this.componentInterface$.id]
        },
        width: '600px'
      });
    }
  }
}
<!-- List of issues on a component/interface -->
<div matSort matSortActive="category">
  <app-query-component errorMessage="Failed to load issues">
    <ng-template appQueryBody>
      <!-- Filter field -->
      <app-issue-filter [projectId]="projectId" [allLabelsList]="allLabelsList" (filterChange)="applyFilter($event)"> </app-issue-filter>

      <!-- Create Issue button -->
      <div style="display: flex; justify-content: flex-start">
        <button
          mat-raised-button
          *ngIf="this.canCreateNewIssue"
          color="primary"
          (click)="this.onAddClick()"
          title="Create a new issue"
          style="margin: 4px 8px 16px 8px; height: 50px"
        >
          Create issue
        </button>
      </div>

      <!-- Issues of the given component / interface -->
      <div class="table-container mat-elevation-z8" *ngIf="list$.hasData">
        <table
          mat-table
          style="width: 100%; min-width: 50%"
          [dataSource]="this.dataSource"
          aria-label="This table shows all issues of the given component or interface."
        >
          <!-- Title column -->
          <ng-container matColumnDef="title">
            <th mat-header-cell *matHeaderCellDef mat-sort-header scope="Title column.">Title</th>
            <td mat-cell style="text-align: left" *matCellDef="let element">{{ element.title }}</td>
          </ng-container>

          <!-- Author column -->
          <ng-container matColumnDef="author">
            <th mat-header-cell *matHeaderCellDef mat-sort-header scope="Author column.">Author</th>
            <td mat-cell style="text-align: left" *matCellDef="let element">{{ element.createdBy.displayName }}</td>
          </ng-container>

          <!-- Assignees column -->
          <ng-container matColumnDef="assignees">
            <th mat-header-cell *matHeaderCellDef mat-sort-header scope="Title column.">Assignees</th>
            <td mat-cell style="text-align: left" *matCellDef="let element">{{ element.assigneesString }}</td>
          </ng-container>

          <!-- Labels column -->
          <ng-container matColumnDef="labels">
            <th mat-header-cell *matHeaderCellDef mat-sort-header scope="Labels column.">Labels</th>
            <td mat-cell style="text-align: left" *matCellDef="let element">
              <div style="float: left" *ngFor="let label of element.labels.nodes">
                <app-issue-label [label]="label"></app-issue-label>
              </div>
            </td>
          </ng-container>

          <!-- Category column -->
          <ng-container matColumnDef="category">
            <th mat-header-cell mat-sort-header *matHeaderCellDef scope="Category column.">Category</th>
            <td mat-cell style="text-align: left" *matCellDef="let element">
              <div style="display: flex; align-items: center">
                <mat-icon [svgIcon]="this.formatCategoryIcon(element)" style="margin-right: 4px"> </mat-icon>
                {{ this.formatCategoryDescription(element.category) }}
              </div>
            </td>
          </ng-container>
          <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
          <tr mat-row *matRowDef="let rowData; columns: columnsToDisplay" (click)="this.clickedOnRow(rowData)"></tr>
        </table>
      </div>

      <app-cursor-paginator [list]="this.list$" [pageSizes]="[10, 25, 50, 100]"></app-cursor-paginator>
    </ng-template>
  </app-query-component>
</div>

./issue-list.component.scss

.mat-row:hover {
  box-shadow: inset 0 0 43px -17px rgba(201, 195, 201, 1);
  cursor: pointer;
}

.table-container {
  overflow: auto;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""