File
Implements
Metadata
selector |
app-issue-detail |
styleUrls |
./issue-detail.component.scss |
templateUrl |
./issue-detail.component.html |
Methods
beginEditing
|
beginEditing()
|
|
Begins the editing process in which:
- the issue title and
- the issue category can be changed.
|
Public
finishEditing
|
finishEditing(save?: boolean)
|
|
Finishes the editing process in which:
- the issue title and
- the issue category can be changed.
Parameters :
Name |
Type |
Optional |
Description |
save |
boolean
|
Yes
|
- Boolean that indicates whether to save the new title.
|
|
formatIssueOpenTime
|
formatIssueOpenTime()
|
|
|
Private
saveChanges
|
saveChanges()
|
|
Saves all changes to the current issue.
|
category
|
Default value : new FormControl('', [Validators.required])
|
|
inputTitle
|
Type : ElementRef
|
Decorators :
@ViewChild('titleInput')
|
|
Public
issueEditable
|
Default value : false
|
|
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>
<!-- 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"> ⋅ {{ 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>
@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 with directive