import { Component , ElementRef , EventEmitter , Input , OnDestroy , OnInit , Output , } from '@angular/core' ; import { DomSanitizer } from '@angular/platform-browser' ; import { Location } from '@angular/common' ; import { CdkDragDrop , moveItemInArray , transferArrayItem } from '@angular/cdk/drag-drop' ; import { ApiResponse , ActivitySimple , Attachment , Board , Category , Column , Comment , Notification , Task , User , UserOptions } from '../../shared/models' ; import { AuthService , ModalService , NotificationsService , StringsService } from '../../shared/services' ; import { BoardService } from '../board.service' ; @ Component ( { selector : 'tb-column' , templateUrl : './column.component.html' , } ) export class ColumnDisplayComponent implements OnInit , OnDestroy { public moveItemInArray : any ; public transferArrayItem : any ; public fileUpload : any ; private subs = [ ] ; public viewTaskActivities : ActivitySimple [ ] ; public fileUploading : boolean ; public showActivity : boolean ; public collapseActivity : boolean ; public isOverdue : boolean ; public isNearlyDue : boolean ; public showLimitEditor : boolean ; public collapseTasks : boolean ; public saving : boolean ; public taskLimit : number ; public taskToRemove : number ; public newComment : string ; public commentOrder : string ; public sortOption : string ; public templateElement : any ; public strings : any ; public commentEdit : Comment ; public commentToRemove : Comment ; public attachmentToRemove : Attachment ; public viewModalProps : Task ; public modalProps : Task ; public userOptions : UserOptions ; public activeUser : User ; public activeBoard : Board ; public quickAdd : Task ; public MODAL_ID : string ; public MODAL_VIEW_ID : string ; public MODAL_CONFIRM_ID : string ; public MODAL_CONFIRM_ATTACHMENT_ID : string ; public MODAL_CONFIRM_COMMENT_ID : string ; @ Input ( 'column' ) columnData : Column ; @ Output ( 'on-update-boards' ) onUpdateBoards : EventEmitter < any > = new EventEmitter < any > ( ) ; constructor ( public elRef , private auth , private location , public notes , public modal , public stringsService , public boardService , private sanitizer ) { this . templateElement = elRef . nativeElement ; this . collapseTasks = false ; this . commentOrder = 'oldest' ; this . sortOption = 'pos' ; this . MODAL_ID = 'add-task-form-' ; this . MODAL_VIEW_ID = 'view-task-form-' ; this . MODAL_CONFIRM_ID = 'task-remove-confirm' ; this . MODAL_CONFIRM_ATTACHMENT_ID = 'attachment-remove-confirm' ; this . MODAL_CONFIRM_COMMENT_ID = 'comment-remove-confirm' ; this . quickAdd = new Task ( ) ; this . modalProps = new Task ( ) ; this . viewModalProps = new Task ( ) ; this . moveItemInArray = moveItemInArray ; this . transferArrayItem = transferArrayItem ; let sub = stringsService . stringsChanged . subscribe ( newStrings => { this . strings = newStrings ; } ) ; this . subs . push ( sub ) ; sub = boardService . activeBoardChanged . subscribe ( newBoard => { this . activeBoard = newBoard ; } ) ; this . subs . push ( sub ) ; sub = boardService . showTaskIdChanged . subscribe ( taskId => { setTimeout ( ( ) => { this . columnData ?. tasks ?. forEach ( task => { if ( + task . id === + taskId ) { this . showViewModal ( + taskId ) ; } } ) ; } , 0 ) ; } ) ; this . subs . push ( sub ) ; sub = auth . userChanged . subscribe ( ( user ) => { if ( user === null ) { return ; } this . activeUser = new User ( + user . default_board_id , user . email , + user . id , user . last_login , + user . security_level , + user . user_option_id , user . username , user . board_access , user . collapsed ) ; this . userOptions = auth . userOptions ; } ) ; this . subs . push ( sub ) ; } ngOnInit ( ) { this . templateElement . classList . remove ( 'double' ) ; if ( this . userOptions && this . userOptions . multiple_tasks_per_row ) { this . templateElement . classList . add ( 'double' ) ; } if ( this . activeUser === undefined ) { return ; } let isCollapsed = false ; this . activeUser . collapsed . forEach ( id => { if ( + id === + this . columnData . id ) { isCollapsed = true ; } } ) ; if ( isCollapsed ) { this . templateElement . classList . add ( 'collapsed' ) ; } this . sortTasks ( ) ; this . taskLimit = this . columnData . task_limit ; } ngOnDestroy ( ) { this . subs . forEach ( sub => ( sub . unsubscribe ( ) ) ) ; } userName ( id ) { const user = this . activeBoard . users . find ( u => u . id === id ) ; return user ? user . username : this . strings . none ; } sortTasks ( ) { switch ( this . sortOption ) { case 'pos' : this . columnData . tasks . sort ( ( a , b ) => { return a . position - b . position ; } ) ; break ; case 'due' : this . columnData . tasks . sort ( ( a , b ) => { return new Date ( a . due_date ) . getTime ( ) - new Date ( b . due_date ) . getTime ( ) ; } ) ; break ; case 'pnt' : this . columnData . tasks . sort ( ( a , b ) => { return b . points - a . points ; } ) ; break ; } } sortComments ( ) { this . viewModalProps . comments . sort ( ( a , b ) => { if ( this . commentOrder === 'oldest' ) { return a . timestamp - b . timestamp ; } return b . timestamp - a . timestamp ; } ) ; } toggleCollapsed ( ) { this . templateElement . classList . toggle ( 'collapsed' ) ; this . boardService . toggleCollapsed ( this . activeUser . id , this . columnData . id ) . subscribe ( ( apiResponse ) => { this . activeUser . collapsed = apiResponse . data [ 1 ] ; this . auth . updateUser ( this . activeUser ) ; } ) ; } toggleTaskCollapse ( ) { this . collapseTasks = ! this . collapseTasks ; } updateTaskColorByCategory ( event ) { this . modalProps . categories = event ; if ( ! event . length ) { return ; } this . modalProps . color = event [ event . length - 1 ] . default_task_color ; } addTask ( newTask = this . modalProps ) { this . saving = true ; if ( ! this . validateTask ( newTask ) ) { this . saving = false ; return ; } this . boardService . addTask ( newTask ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { this . saving = false ; return ; } this . modal . close ( this . MODAL_ID + this . columnData . id + '' ) ; const boardData = response . data [ 2 ] [ 0 ] ; boardData . ownColumn . forEach ( ( column ) => { if ( ! column . ownTask ) { column . ownTask = [ ] ; } } ) ; this . boardService . updateActiveBoard ( boardData ) ; this . onUpdateBoards . next ( ) ; this . saving = false ; } , err => { this . notes . add ( { type : 'error' , text : err . toString ( ) } ) ; } ) ; } drop ( event ) { if ( event . previousContainer === event . container ) { this . moveItemInArray ( event . container . data , event . previousIndex , event . currentIndex ) ; } else { this . transferArrayItem ( event . previousContainer . data , event . container . data , event . previousIndex , event . currentIndex ) ; } const colId = event . container . id . substr ( 3 ) as unknown as number - 1 ; const column = this . activeBoard . columns [ colId ] ; column . tasks . forEach ( ( task , index ) => { task . position = index + 1 ; } ) ; this . boardService . updateColumn ( column ) . subscribe ( ( response ) => { if ( response . status !== 'success' ) { response . alerts . forEach ( note => this . notes . add ( note ) ) ; return ; } this . boardService . updateActiveBoard ( response . data [ 2 ] [ 0 ] ) ; } ) ; } updateTask ( ) { this . saving = true ; if ( ! this . validateTask ( this . modalProps ) ) { this . saving = false ; return ; } this . boardService . updateTask ( this . modalProps ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { this . saving = false ; return ; } this . boardService . updateActiveBoard ( response . data [ 2 ] [ 0 ] ) ; this . onUpdateBoards . next ( ) ; this . modal . close ( this . MODAL_ID + this . columnData ?. id + '' ) ; this . saving = false ; } ) ; } removeTask ( ) { this . boardService . removeTask ( this . taskToRemove ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { return ; } this . boardService . updateActiveBoard ( response . data [ 1 ] [ 0 ] ) ; this . onUpdateBoards . next ( ) ; } ) ; } fileChange ( file ) { this . fileUpload = file ; } addFile ( ) { if ( ! this . fileUpload ) { this . notes . add ( { type : 'error' , text : this . strings . boards_taskNoFileError } ) ; return ; } this . fileUploading = true ; const attachment = new Attachment ( ) ; attachment . filename = this . fileUpload . name ; attachment . name = attachment . filename . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' ) ; attachment . type = this . fileUpload . type ; attachment . user_id = this . activeUser . id ; attachment . task_id = this . viewModalProps . id ; this . boardService . addAttachment ( attachment ) . subscribe ( response => { if ( response . status !== 'success' ) { this . fileUploading = false ; this . resetFileInput ( ) ; return ; } attachment . id = response . data [ 1 ] . id ; attachment . diskfilename = response . data [ 1 ] . diskfilename ; this . uploadFile ( attachment , response ) ; } ) ; } viewFile ( hash ) { window . open ( `./files/${ hash }` , 'tb-file-view' ) ; } getUrl ( hash ) { const url = `./api/uploads/${ hash }` ; return this . sanitizer . bypassSecurityTrustResourceUrl ( url ) ; } removeAttachment ( ) { this . boardService . removeAttachment ( this . attachmentToRemove . id ) . subscribe ( res => { res . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( res . status === 'success' ) { const index = this . viewModalProps . attachments . findIndex ( x => x . id === this . attachmentToRemove . id ) ; this . viewModalProps . attachments . splice ( index , 1 ) ; this . updateTaskActivity ( this . viewModalProps . id ) ; } } ) ; } addComment ( ) { if ( this . viewModalProps . id < 1 ) { return ; } const comment = new Comment ( 0 , this . newComment , this . activeUser . id , this . viewModalProps . id ) ; this . newComment = '' ; this . boardService . addComment ( comment ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { return ; } const updatedTask = response . data [ 1 ] [ 0 ] ; this . replaceUpdatedTask ( updatedTask ) ; this . viewModalProps = this . convertToTask ( updatedTask ) ; this . updateTaskActivity ( this . viewModalProps . id ) ; } ) ; } beginEditComment ( comment ) { this . commentEdit = { ... comment } ; } editComment ( ) { this . commentEdit . is_edited = true ; this . commentEdit . user_id = this . activeUser . id ; this . boardService . updateComment ( this . commentEdit ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { return ; } const updatedTask = response . data [ 1 ] [ 0 ] ; this . replaceUpdatedTask ( updatedTask ) ; this . viewModalProps = this . convertToTask ( updatedTask ) ; this . updateTaskActivity ( this . viewModalProps . id ) ; } ) ; } removeComment ( ) { for ( let i = this . viewModalProps . comments . length - 1 ; i >= 0 ; -- i ) { if ( this . viewModalProps . comments [ i ] . id === this . commentToRemove . id ) { this . viewModalProps . comments . splice ( i , 1 ) ; } } this . boardService . removeComment ( this . commentToRemove . id ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; const updatedTask = response . data [ 1 ] [ 0 ] ; this . replaceUpdatedTask ( updatedTask ) ; this . updateTaskActivity ( this . viewModalProps . id ) ; } ) ; } canAdminComment ( comment ) { if ( this . activeUser . id === comment . user_id ) { return true ; } return this . activeUser . isAnyAdmin ( ) ; } beginLimitEdit ( ) { this . taskLimit = this . columnData . task_limit ; this . showLimitEditor = true ; } cancelLimitChanges ( ) { this . showLimitEditor = false ; } saveLimitChanges ( ) { const originalLimit = this . columnData . task_limit ; this . columnData . task_limit = this . taskLimit ; this . boardService . updateColumn ( this . columnData ) . subscribe ( ( response ) => { response . alerts . forEach ( note => this . notes . add ( note ) ) ; if ( response . status !== 'success' ) { this . columnData . task_limit = originalLimit ; return ; } const colData = response . data [ 1 ] [ 0 ] ; this . columnData = new Column ( colData . id , colData . name , colData . position , colData . board_id , colData . task_limit , colData . ownTask ) ; } ) ; this . showLimitEditor = false ; } getTextColor ( color ) { const r = parseInt ( color . substr ( 1 , 2 ) , 16 ) ; const g = parseInt ( color . substr ( 3 , 2 ) , 16 ) ; const b = parseInt ( color . substr ( 5 , 2 ) , 16 ) ; const yiq = ( ( r * 299 ) + ( g * 587 ) + ( b * 114 ) ) / 1000 ; return yiq >= 140 ? '#333333' : '#efefef' ; } quickAddClicked ( event ) { this . preventEnter ( event ) ; if ( this . quickAdd . title === '' ) { this . showModal ( ) ; return ; } this . quickAdd . column_id = this . columnData . id ; this . addTask ( this . quickAdd ) ; this . quickAdd = new Task ( ) ; } checkDueDate ( ) { if ( this . viewModalProps . due_date === '' ) { return ; } const dueDate = new Date ( this . viewModalProps . due_date ) ; if ( isNaN ( dueDate . valueOf ( ) ) ) { return ; } const millisecondsPerDay = ( 1000 * 3600 * 24 ) ; const today = new Date ( ) ; const timeDiff = today . getTime ( ) - dueDate . getTime ( ) ; const daysDiff = Math . ceil ( timeDiff / millisecondsPerDay ) ; if ( daysDiff > 0 ) { this . isOverdue = true ; } if ( daysDiff <= 0 && daysDiff > - 3 ) { this . isNearlyDue = true ; } } getRemoveTaskFunction ( taskId ) { return ( ) => { this . taskToRemove = taskId ; this . modal . open ( this . MODAL_CONFIRM_ID + this . columnData . id ) ; } ; } getShowModalFunction ( taskId = 0 ) { return ( ) => { this . showModal ( taskId ) ; } ; } getShowViewModalFunction ( taskId ) { return ( ) => { const url = 'boards/' + this . activeBoard . id + '/' + taskId ; this . location . go ( url ) ; this . showViewModal ( taskId ) ; } ; } showModal ( taskId = 0 ) { if ( taskId === 0 ) { this . modalProps = new Task ( ) ; this . modalProps . column_id = this . columnData . id ; this . modal . open ( this . MODAL_ID + this . columnData . id ) ; return ; } const editTask = this . columnData . tasks . find ( task => task . id === taskId ) ; this . modalProps = new Task ( editTask . id , editTask . title , editTask . description , editTask . color , editTask . due_date , editTask . points , editTask . position , editTask . column_id , editTask . comments , editTask . attachments , [ ] , [ ] ) ; this . activeBoard . users . forEach ( user => { editTask . assignees . forEach ( assignee => { if ( assignee . id === user . id ) { this . modalProps . assignees . push ( user ) ; } } ) ; } ) ; this . activeBoard . categories . forEach ( category => { editTask . categories . forEach ( cat => { if ( cat . id === category . id ) { this . modalProps . categories . push ( category ) ; } } ) ; } ) ; this . modal . open ( this . MODAL_ID + this . columnData . id ) ; } getUserName ( userId ) { const user = this . activeBoard . users . find ( ( test ) => test . id === + userId ) ; return user . username ; } callBoardUpdate ( ) { this . onUpdateBoards . emit ( ) ; } showViewModal ( taskId ) { const viewTask = this . columnData . tasks . find ( task => task . id === taskId ) ; this . updateTaskActivity ( taskId ) ; this . boardService . convertMarkdown ( viewTask . description , this . markedCallback ) . then ( data => { data . html . replace ( /(\{)([^}]+)(\})/g , '{{ "{" }}$2{{ "}"  }}' ) ; this . viewModalProps . html = this . sanitizer . bypassSecurityTrustHtml ( data . html ) ; } ) ; viewTask . comments . forEach ( comment => { this . boardService . convertMarkdown ( comment . text , this . markedCallback ) . then ( data => { comment . html = this . sanitizer . bypassSecurityTrustHtml ( data . html ) ; } ) ; } ) ; this . newComment = '' ; this . viewModalProps = Object . assign ( { } , viewTask ) ; this . checkDueDate ( ) ; this . showActivity = false ; setTimeout ( ( ) => ( this . showActivity = true ) , 500 ) ; this . modal . open ( this . MODAL_VIEW_ID + this . columnData . id ) ; } preventEnter ( event ) { if ( event && event . stopPropagation ) { event . stopPropagation ( ) ; } } private uploadFile ( attachment , response ) { const data = new FormData ( ) ; data . append ( 'file' , this . fileUpload ) ; this . boardService . uploadAttachment ( data , attachment . diskfilename ) . subscribe ( res => { res . alerts . forEach ( note => this . notes . add ( note ) ) ; this . fileUploading = false ; this . resetFileInput ( ) ; if ( res . status === 'success' ) { response . alerts . forEach ( note => this . notes . add ( note ) ) ; this . viewModalProps . attachments . push ( attachment ) ; this . updateTaskActivity ( this . viewModalProps . id ) ; } } ) ; } private resetFileInput ( ) { const upload = document . getElementsByClassName ( 'fileuploadinput' ) ; Array . from ( upload ) . forEach ( ( input ) => { input . value = '' ; } ) ; } private updateTaskActivity ( id ) { this . viewTaskActivities = [ ] ; if ( this . activeUser . security_level > 2 ) { return ; } this . boardService . getTaskActivity ( id ) . subscribe ( response => { if ( response . data . length === 0 ) { return ; } response . data [ 1 ] . forEach ( ( item ) => { this . viewTaskActivities . push ( new ActivitySimple ( item . text , item . timestamp ) ) ; } ) ; } ) ; } private convertToTask ( updatedTask ) { const task = new Task ( updatedTask . id , updatedTask . title , updatedTask . description , updatedTask . color , updatedTask . due , updatedTask . points , updatedTask . position , updatedTask . column_id , updatedTask . ownComment , updatedTask . ownAttachment , updatedTask . sharedUser , updatedTask . sharedCategory ) ; task . html = updatedTask . html ; task . comments . forEach ( comment => { this . boardService . convertMarkdown ( comment . text , this . markedCallback ) . then ( data => { comment . html = data . html ; } ) ; } ) ; return task ; } private replaceUpdatedTask ( updatedTask ) { const oldTask = this . activeBoard . columns . find ( column => + column . id === + updatedTask . column_id ) . tasks . find ( task => + task . id === + updatedTask . id ) ; updatedTask . html = oldTask . html ; this . updateTaskComments ( oldTask , updatedTask . ownComment ) ; } private updateTaskComments ( task , newComments ) { task . comments = [ ] ; if ( ! newComments ) { return ; } newComments . forEach ( comment => { task . comments . push ( new Comment ( comment . id , comment . text , comment . user_id , comment . task_id , comment . timestamp ) ) ; } ) ; this . sortComments ( ) ; } private markedCallback = ( _ , text ) => { this . activeBoard . issue_trackers ?. forEach ( tracker => { const re = new RegExp ( tracker . regex , 'ig' ) ; const replacements = new Array < any > ( ) ; let result = re . exec ( text ) ; while ( result !== null ) { const link = '<a href="' + tracker . url . replace ( /%BUGID%/g , result [ 1 ] ) + '" target="tb_external" rel="noreferrer">' + result [ 0 ] + '</a>' ; replacements . push ( { str : result [ 0 ] , link } ) ; result = re . exec ( text ) ; } for ( let i = replacements . length - 1 ; i >= 0 ; -- i ) { text = text . replace ( replacements [ i ] . str , replacements [ i ] . link ) ; } } ) ; return text ; } ; private validateTask ( task ) { if ( task . title === '' ) { this . notes . add ( new Notification ( 'error' , 'Task title is required.' ) ) ; return false ; } return true ; } }