mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2025-07-19 14:50:47 +00:00
527 lines
15 KiB
TypeScript
527 lines
15 KiB
TypeScript
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
|
import { TemplatePortal } from '@angular/cdk/portal';
|
|
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
|
import { MatDrawerToggleResult } from '@angular/material/sidenav';
|
|
import { Subject } from 'rxjs';
|
|
import { debounceTime, filter, takeUntil, tap } from 'rxjs/operators';
|
|
import { assign } from 'lodash-es';
|
|
import * as moment from 'moment';
|
|
import { Tag, Task } from 'app/modules/admin/apps/tasks/tasks.types';
|
|
import { TasksListComponent } from 'app/modules/admin/apps/tasks/list/list.component';
|
|
import { TasksService } from 'app/modules/admin/apps/tasks/tasks.service';
|
|
|
|
@Component({
|
|
selector : 'tasks-details',
|
|
templateUrl : './details.component.html',
|
|
encapsulation : ViewEncapsulation.None,
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
export class TasksDetailsComponent implements OnInit, AfterViewInit, OnDestroy
|
|
{
|
|
@ViewChild('tagsPanelOrigin') private _tagsPanelOrigin: ElementRef;
|
|
@ViewChild('tagsPanel') private _tagsPanel: TemplateRef<any>;
|
|
@ViewChild('titleField') private _titleField: ElementRef;
|
|
|
|
tags: Tag[];
|
|
tagsEditMode: boolean = false;
|
|
filteredTags: Tag[];
|
|
task: Task;
|
|
taskForm: FormGroup;
|
|
tasks: Task[];
|
|
private _tagsPanelOverlayRef: OverlayRef;
|
|
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor(
|
|
private _activatedRoute: ActivatedRoute,
|
|
private _changeDetectorRef: ChangeDetectorRef,
|
|
private _formBuilder: FormBuilder,
|
|
private _renderer2: Renderer2,
|
|
private _router: Router,
|
|
private _tasksListComponent: TasksListComponent,
|
|
private _tasksService: TasksService,
|
|
private _overlay: Overlay,
|
|
private _viewContainerRef: ViewContainerRef
|
|
)
|
|
{
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Lifecycle hooks
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* On init
|
|
*/
|
|
ngOnInit(): void
|
|
{
|
|
// Open the drawer
|
|
this._tasksListComponent.matDrawer.open();
|
|
|
|
// Create the task form
|
|
this.taskForm = this._formBuilder.group({
|
|
id : [''],
|
|
type : [''],
|
|
title : [''],
|
|
notes : [''],
|
|
completed: [false],
|
|
dueDate : [null],
|
|
priority : [0],
|
|
tags : [[]],
|
|
order : [0]
|
|
});
|
|
|
|
// Get the tags
|
|
this._tasksService.tags$
|
|
.pipe(takeUntil(this._unsubscribeAll))
|
|
.subscribe((tags: Tag[]) => {
|
|
this.tags = tags;
|
|
this.filteredTags = tags;
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
});
|
|
|
|
// Get the tasks
|
|
this._tasksService.tasks$
|
|
.pipe(takeUntil(this._unsubscribeAll))
|
|
.subscribe((tasks: Task[]) => {
|
|
this.tasks = tasks;
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
});
|
|
|
|
// Get the task
|
|
this._tasksService.task$
|
|
.pipe(takeUntil(this._unsubscribeAll))
|
|
.subscribe((task: Task) => {
|
|
|
|
// Open the drawer in case it is closed
|
|
this._tasksListComponent.matDrawer.open();
|
|
|
|
// Get the task
|
|
this.task = task;
|
|
|
|
// Patch values to the form from the task
|
|
this.taskForm.patchValue(task, {emitEvent: false});
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
});
|
|
|
|
// Update task when there is a value change on the task form
|
|
this.taskForm.valueChanges
|
|
.pipe(
|
|
tap((value) => {
|
|
|
|
// Update the task object
|
|
this.task = assign(this.task, value);
|
|
}),
|
|
debounceTime(300),
|
|
takeUntil(this._unsubscribeAll)
|
|
)
|
|
.subscribe((value) => {
|
|
|
|
// Update the task on the server
|
|
this._tasksService.updateTask(value.id, value).subscribe();
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
});
|
|
|
|
// Listen for NavigationEnd event to focus on the title field
|
|
this._router.events
|
|
.pipe(
|
|
takeUntil(this._unsubscribeAll),
|
|
filter(event => event instanceof NavigationEnd)
|
|
)
|
|
.subscribe(() => {
|
|
|
|
// Focus on the title field
|
|
this._titleField.nativeElement.focus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* After view init
|
|
*/
|
|
ngAfterViewInit(): void
|
|
{
|
|
// Listen for matDrawer opened change
|
|
this._tasksListComponent.matDrawer.openedChange
|
|
.pipe(
|
|
takeUntil(this._unsubscribeAll),
|
|
filter(opened => opened)
|
|
)
|
|
.subscribe(() => {
|
|
|
|
// Focus on the title element
|
|
this._titleField.nativeElement.focus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* On destroy
|
|
*/
|
|
ngOnDestroy(): void
|
|
{
|
|
// Unsubscribe from all subscriptions
|
|
this._unsubscribeAll.next();
|
|
this._unsubscribeAll.complete();
|
|
|
|
// Dispose the overlay
|
|
if ( this._tagsPanelOverlayRef )
|
|
{
|
|
this._tagsPanelOverlayRef.dispose();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Public methods
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Close the drawer
|
|
*/
|
|
closeDrawer(): Promise<MatDrawerToggleResult>
|
|
{
|
|
return this._tasksListComponent.matDrawer.close();
|
|
}
|
|
|
|
/**
|
|
* Toggle the completed status
|
|
*/
|
|
toggleCompleted(): void
|
|
{
|
|
// Get the form control for 'completed'
|
|
const completedFormControl = this.taskForm.get('completed');
|
|
|
|
// Toggle the completed status
|
|
completedFormControl.setValue(!completedFormControl.value);
|
|
}
|
|
|
|
/**
|
|
* Open tags panel
|
|
*/
|
|
openTagsPanel(): void
|
|
{
|
|
// Create the overlay
|
|
this._tagsPanelOverlayRef = this._overlay.create({
|
|
backdropClass : '',
|
|
hasBackdrop : true,
|
|
scrollStrategy : this._overlay.scrollStrategies.block(),
|
|
positionStrategy: this._overlay.position()
|
|
.flexibleConnectedTo(this._tagsPanelOrigin.nativeElement)
|
|
.withFlexibleDimensions()
|
|
.withViewportMargin(64)
|
|
.withLockedPosition()
|
|
.withPositions([
|
|
{
|
|
originX : 'start',
|
|
originY : 'bottom',
|
|
overlayX: 'start',
|
|
overlayY: 'top'
|
|
}
|
|
])
|
|
});
|
|
|
|
// Subscribe to the attachments observable
|
|
this._tagsPanelOverlayRef.attachments().subscribe(() => {
|
|
|
|
// Focus to the search input once the overlay has been attached
|
|
this._tagsPanelOverlayRef.overlayElement.querySelector('input').focus();
|
|
});
|
|
|
|
// Create a portal from the template
|
|
const templatePortal = new TemplatePortal(this._tagsPanel, this._viewContainerRef);
|
|
|
|
// Attach the portal to the overlay
|
|
this._tagsPanelOverlayRef.attach(templatePortal);
|
|
|
|
// Subscribe to the backdrop click
|
|
this._tagsPanelOverlayRef.backdropClick().subscribe(() => {
|
|
|
|
// If overlay exists and attached...
|
|
if ( this._tagsPanelOverlayRef && this._tagsPanelOverlayRef.hasAttached() )
|
|
{
|
|
// Detach it
|
|
this._tagsPanelOverlayRef.detach();
|
|
|
|
// Reset the tag filter
|
|
this.filteredTags = this.tags;
|
|
|
|
// Toggle the edit mode off
|
|
this.tagsEditMode = false;
|
|
}
|
|
|
|
// If template portal exists and attached...
|
|
if ( templatePortal && templatePortal.isAttached )
|
|
{
|
|
// Detach it
|
|
templatePortal.detach();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle the tags edit mode
|
|
*/
|
|
toggleTagsEditMode(): void
|
|
{
|
|
this.tagsEditMode = !this.tagsEditMode;
|
|
}
|
|
|
|
/**
|
|
* Filter tags
|
|
*
|
|
* @param event
|
|
*/
|
|
filterTags(event): void
|
|
{
|
|
// Get the value
|
|
const value = event.target.value.toLowerCase();
|
|
|
|
// Filter the tags
|
|
this.filteredTags = this.tags.filter(tag => tag.title.toLowerCase().includes(value));
|
|
}
|
|
|
|
/**
|
|
* Filter tags input key down event
|
|
*
|
|
* @param event
|
|
*/
|
|
filterTagsInputKeyDown(event): void
|
|
{
|
|
// Return if the pressed key is not 'Enter'
|
|
if ( event.key !== 'Enter' )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If there is no tag available...
|
|
if ( this.filteredTags.length === 0 )
|
|
{
|
|
// Create the tag
|
|
this.createTag(event.target.value);
|
|
|
|
// Clear the input
|
|
event.target.value = '';
|
|
|
|
// Return
|
|
return;
|
|
}
|
|
|
|
// If there is a tag...
|
|
const tag = this.filteredTags[0];
|
|
const isTagApplied = this.task.tags.find((id) => id === tag.id);
|
|
|
|
// If the found tag is already applied to the task...
|
|
if ( isTagApplied )
|
|
{
|
|
// Remove the tag from the task
|
|
this.deleteTagFromTask(tag);
|
|
}
|
|
else
|
|
{
|
|
// Otherwise add the tag to the task
|
|
this.addTagToTask(tag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new tag
|
|
*
|
|
* @param title
|
|
*/
|
|
createTag(title: string): void
|
|
{
|
|
const tag = {
|
|
title
|
|
};
|
|
|
|
// Create tag on the server
|
|
this._tasksService.createTag(tag)
|
|
.subscribe((response) => {
|
|
|
|
// Add the tag to the task
|
|
this.addTagToTask(response);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update the tag title
|
|
*
|
|
* @param tag
|
|
* @param event
|
|
*/
|
|
updateTagTitle(tag: Tag, event): void
|
|
{
|
|
// Update the title on the tag
|
|
tag.title = event.target.value;
|
|
|
|
// Update the tag on the server
|
|
this._tasksService.updateTag(tag.id, tag)
|
|
.pipe(debounceTime(300))
|
|
.subscribe();
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Delete the tag
|
|
*
|
|
* @param tag
|
|
*/
|
|
deleteTag(tag: Tag): void
|
|
{
|
|
// Delete the tag from the server
|
|
this._tasksService.deleteTag(tag.id).subscribe();
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Add tag to the task
|
|
*
|
|
* @param tag
|
|
*/
|
|
addTagToTask(tag: Tag): void
|
|
{
|
|
// Add the tag
|
|
this.task.tags.unshift(tag.id);
|
|
|
|
// Update the task form
|
|
this.taskForm.get('tags').patchValue(this.task.tags);
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Delete tag from the task
|
|
*
|
|
* @param tag
|
|
*/
|
|
deleteTagFromTask(tag: Tag): void
|
|
{
|
|
// Remove the tag
|
|
this.task.tags.splice(this.task.tags.findIndex(item => item === tag.id), 1);
|
|
|
|
// Update the task form
|
|
this.taskForm.get('tags').patchValue(this.task.tags);
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Toggle task tag
|
|
*
|
|
* @param tag
|
|
*/
|
|
toggleTaskTag(tag: Tag): void
|
|
{
|
|
if ( this.task.tags.includes(tag.id) )
|
|
{
|
|
this.deleteTagFromTask(tag);
|
|
}
|
|
else
|
|
{
|
|
this.addTagToTask(tag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should the create tag button be visible
|
|
*
|
|
* @param inputValue
|
|
*/
|
|
shouldShowCreateTagButton(inputValue: string): boolean
|
|
{
|
|
return !!!(inputValue === '' || this.tags.findIndex(tag => tag.title.toLowerCase() === inputValue.toLowerCase()) > -1);
|
|
}
|
|
|
|
/**
|
|
* Set the task priority
|
|
*
|
|
* @param priority
|
|
*/
|
|
setTaskPriority(priority): void
|
|
{
|
|
// Set the value
|
|
this.taskForm.get('priority').setValue(priority);
|
|
}
|
|
|
|
/**
|
|
* Check if the task is overdue or not
|
|
*/
|
|
isOverdue(): boolean
|
|
{
|
|
return moment(this.task.dueDate, moment.ISO_8601).isBefore(moment(), 'days');
|
|
}
|
|
|
|
/**
|
|
* Delete the task
|
|
*/
|
|
deleteTask(): void
|
|
{
|
|
// Get the current task's id
|
|
const id = this.task.id;
|
|
|
|
// Get the next/previous task's id
|
|
const currentTaskIndex = this.tasks.findIndex(item => item.id === id);
|
|
const nextTaskIndex = currentTaskIndex + ((currentTaskIndex === (this.tasks.length - 1)) ? -1 : 1);
|
|
const nextTaskId = (this.tasks.length === 1 && this.tasks[0].id === id) ? null : this.tasks[nextTaskIndex].id;
|
|
|
|
// Delete the task
|
|
this._tasksService.deleteTask(id)
|
|
.subscribe((isDeleted) => {
|
|
|
|
// Return if the task wasn't deleted...
|
|
if ( !isDeleted )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the current activated route
|
|
let route = this._activatedRoute;
|
|
while ( route.firstChild )
|
|
{
|
|
route = route.firstChild;
|
|
}
|
|
|
|
// Navigate to the next task if available
|
|
if ( nextTaskId )
|
|
{
|
|
this._router.navigate(['../', nextTaskId], {relativeTo: route});
|
|
}
|
|
// Otherwise, navigate to the parent
|
|
else
|
|
{
|
|
this._router.navigate(['../'], {relativeTo: route});
|
|
}
|
|
});
|
|
|
|
// Mark for check
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Track by function for ngFor loops
|
|
*
|
|
* @param index
|
|
* @param item
|
|
*/
|
|
trackByFn(index: number, item: any): any
|
|
{
|
|
return item.id || index;
|
|
}
|
|
}
|