Attachments fully working

This commit is contained in:
Matthew Ross 2020-05-11 19:10:36 -04:00
parent 5748f762e9
commit a109c9d0a5
8 changed files with 203 additions and 69 deletions

View File

@ -35,6 +35,7 @@ export const ROUTES: Routes = [
{ {
path: 'files/:hash', path: 'files/:hash',
component: FileViewerComponent, component: FileViewerComponent,
canActivate: [ AuthGuard ]
} }
]; ];

View File

@ -123,6 +123,14 @@ export class BoardService {
); );
} }
addComment(comment: Comment): Observable<ApiResponse> {
return this.http.post('api/comments', comment)
.pipe(
map((response: ApiResponse) => response),
catchError((err) => of(err.error as ApiResponse))
);
}
updateComment(comment: Comment): Observable<ApiResponse> { updateComment(comment: Comment): Observable<ApiResponse> {
return this.http.post('api/comments/' + comment.id, comment) return this.http.post('api/comments/' + comment.id, comment)
.pipe( .pipe(
@ -139,8 +147,7 @@ export class BoardService {
); );
} }
/* istanbul ignore next */ addAttachment(attachment: Attachment): Observable<ApiResponse> {
uploadAttachment(attachment: Attachment): Observable<ApiResponse> {
return this.http.post('api/attachments', attachment) return this.http.post('api/attachments', attachment)
.pipe( .pipe(
map((response: ApiResponse) => response), map((response: ApiResponse) => response),
@ -148,6 +155,14 @@ export class BoardService {
); );
} }
uploadAttachment(data: FormData, hash: string): Observable<ApiResponse> {
return this.http.post('api/upload/' + hash, data)
.pipe(
map((response: ApiResponse) => response),
catchError((err) => of(err.error as ApiResponse))
);
}
removeAttachment(id: number): Observable<ApiResponse> { removeAttachment(id: number): Observable<ApiResponse> {
return this.http.delete('api/attachments/' + id) return this.http.delete('api/attachments/' + id)
.pipe( .pipe(

View File

@ -178,8 +178,11 @@
<i class="icon icon-eye" (click)="viewFile(item.diskfilename)" <i class="icon icon-eye" (click)="viewFile(item.diskfilename)"
[title]="strings['boards_taskView'] + ' ' + item.filename"></i> [title]="strings['boards_taskView'] + ' ' + item.filename"></i>
<i class="icon icon-download" [title]="strings['boards_taskDownload'] <a [href]="getUrl(item.diskfilename)" download="{{ item.filename }}">
+ ' ' + item.filename"></i> <i class="icon icon-download"
[title]="strings['boards_taskDownload'] + ' ' +
item.filename"></i>
</a>
<i class="icon icon-trash-empty" <i class="icon icon-trash-empty"
[title]="strings['settings_remove'] + ' ' + item.filename" [title]="strings['settings_remove'] + ' ' + item.filename"
@ -191,12 +194,14 @@
</div> </div>
</div> </div>
<div> <div class="file-upload">
<h3>{{ strings['boards_taskAddAttachment'] }}</h3> <h3>{{ strings['boards_taskAddAttachment'] }}</h3>
<input type="file" #fileupload (change)="fileChange(fileupload.files[0])"> <input type="file" class="fileuploadinput" #fileupload
(change)="fileChange(fileupload.files[0])">
<button (click)="uploadFile()" [disabled]="!fileupload.files[0]"> <button (click)="addFile()"
[disabled]="!fileupload.files[0] || fileUploading">
<i class="icon icon-upload"></i> <i class="icon icon-upload"></i>
{{ strings['boards_taskUpload'] }} {{ strings['boards_taskUpload'] }}

View File

@ -7,7 +7,7 @@ import {
OnDestroy, OnDestroy,
Output Output
} from '@angular/core'; } from '@angular/core';
import { DomSanitizer, } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { import {
CdkDragDrop, CdkDragDrop,
moveItemInArray, moveItemInArray,
@ -40,12 +40,15 @@ import { BoardService } from '../board.service';
templateUrl: './column.component.html' templateUrl: './column.component.html'
}) })
export class ColumnDisplayComponent implements OnInit, OnDestroy { export class ColumnDisplayComponent implements OnInit, OnDestroy {
public moveItemInArray: any;
public transferArrayItem: any;
public fileUpload: any;
private fileUpload: any;
private subs = []; private subs = [];
public viewTaskActivities: ActivitySimple[]; public viewTaskActivities: ActivitySimple[];
public fileUploading: boolean;
public showActivity: boolean; public showActivity: boolean;
public collapseActivity: boolean; public collapseActivity: boolean;
public isOverdue: boolean; public isOverdue: boolean;
@ -88,7 +91,7 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
constructor(public elRef: ElementRef, constructor(public elRef: ElementRef,
private auth: AuthService, private auth: AuthService,
private notes: NotificationsService, public notes: NotificationsService,
public modal: ModalService, public modal: ModalService,
public stringsService: StringsService, public stringsService: StringsService,
public boardService: BoardService, public boardService: BoardService,
@ -107,6 +110,9 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
this.modalProps = new Task(); this.modalProps = new Task();
this.viewModalProps = new Task(); this.viewModalProps = new Task();
this.moveItemInArray = moveItemInArray;
this.transferArrayItem = transferArrayItem;
let sub = stringsService.stringsChanged.subscribe(newStrings => { let sub = stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings; this.strings = newStrings;
}); });
@ -231,9 +237,7 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
return; return;
} }
this.modal.close(this.MODAL_ID + (this.columnData this.modal.close(this.MODAL_ID + this.columnData.id + '');
? this.columnData.id + ''
: ''));
const boardData = response.data[2][0]; const boardData = response.data[2][0];
boardData.ownColumn.forEach((column: any) => { boardData.ownColumn.forEach((column: any) => {
@ -252,10 +256,10 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
drop(event: CdkDragDrop<string[]>, colIndex: number) { drop(event: CdkDragDrop<string[]>, colIndex: number) {
if (event.previousContainer === event.container) { if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, this.moveItemInArray(event.container.data,
event.previousIndex, event.currentIndex); event.previousIndex, event.currentIndex);
} else { } else {
transferArrayItem(event.previousContainer.data, this.transferArrayItem(event.previousContainer.data,
event.container.data, event.previousIndex, event.currentIndex); event.container.data, event.previousIndex, event.currentIndex);
} }
@ -286,9 +290,7 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
} }
this.boardService.updateActiveBoard(response.data[2][0]); this.boardService.updateActiveBoard(response.data[2][0]);
this.modal.close(this.MODAL_ID + (this.columnData this.modal.close(this.MODAL_ID + this.columnData?.id + '');
? this.columnData.id + ''
: ''));
this.boardService.refreshToken(); this.boardService.refreshToken();
this.saving = false; this.saving = false;
@ -309,60 +311,63 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
}); });
} }
/* istanbul ignore next */
fileChange(file: File) { fileChange(file: File) {
this.fileUpload = file; this.fileUpload = file;
} }
/* istanbul ignore next */ addFile() {
uploadFile() {
if (!this.fileUpload) { if (!this.fileUpload) {
this.notes.add({ type: 'error', text: this.strings.boards_taskNoFileError }); this.notes
.add({ type: 'error', text: this.strings.boards_taskNoFileError });
return; return;
} }
const fileReader = new FileReader(); this.fileUploading = true;
fileReader.onload = () => { const attachment = new Attachment();
const attachment = new Attachment();
attachment.filename = this.fileUpload.name; attachment.filename = this.fileUpload.name;
attachment.name = attachment.filename.split('.').slice(0, -1).join('.'); attachment.name = attachment.filename.split('.').slice(0, -1).join('.');
attachment.type = this.fileUpload.type; attachment.type = this.fileUpload.type;
attachment.user_id = this.activeUser.id; attachment.user_id = this.activeUser.id;
attachment.task_id = this.viewModalProps.id; attachment.task_id = this.viewModalProps.id;
attachment.data = fileReader.result;
this.boardService.uploadAttachment(attachment) this.boardService.addAttachment(attachment).subscribe(response => {
.subscribe(response => { if (response.status !== 'success') {
response.alerts.forEach(note => this.notes.add(note)); this.fileUploading = false;
this.resetFileInput();
if (response.status === 'success') { return;
attachment.id = response.data[1].id; }
attachment.diskfilename = response.data[1].diskfilename;
this.viewModalProps.attachments.push(attachment); attachment.id = response.data[1].id;
} attachment.diskfilename = response.data[1].diskfilename;
});
}
fileReader.readAsBinaryString(this.fileUpload); this.uploadFile(attachment, response);
});
} }
viewFile(hash: string) { viewFile(hash: string) {
window.open(`./files/${hash}`, 'tb-file-view'); window.open(`./files/${hash}`, 'tb-file-view');
} }
getUrl(hash: string) {
const url = `./api/uploads/${hash}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
removeAttachment() { removeAttachment() {
this.boardService.removeAttachment(this.attachmentToRemove.id).subscribe(res => { this.boardService.removeAttachment(this.attachmentToRemove.id)
res.alerts.forEach(note => this.notes.add(note)); .subscribe(res => {
res.alerts.forEach(note => this.notes.add(note));
if (res.status === 'success') { if (res.status === 'success') {
const index = this.viewModalProps.attachments const index = this.viewModalProps.attachments
.findIndex(x => x.id === this.attachmentToRemove.id); .findIndex(x => x.id === this.attachmentToRemove.id);
this.viewModalProps.attachments.splice(index, 1); this.viewModalProps.attachments.splice(index, 1);
} this.updateTaskActivity(this.viewModalProps.id);
}); }
});
} }
addComment() { addComment() {
@ -370,14 +375,15 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
return; return;
} }
this.viewModalProps.comments.push( const comment = new Comment(0, this.newComment, this.activeUser.id,
new Comment(0, this.newComment, this.activeUser.id, this.viewModalProps.id);
this.viewModalProps.id));
this.newComment = ''; this.newComment = '';
this.boardService.updateTask(this.viewModalProps) this.boardService.addComment(comment)
.subscribe((response: ApiResponse) => { .subscribe((response: ApiResponse) => {
response.alerts.forEach(note => this.notes.add(note));
if (response.status !== 'success') { if (response.status !== 'success') {
return; return;
} }
@ -582,7 +588,8 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
} }
getUserName(userId: number) { getUserName(userId: number) {
const user = this.activeBoard.users.find((test: User) => test.id === +userId); const user = this.activeBoard.users
.find((test: User) => test.id === +userId);
return user.username; return user.username;
} }
@ -614,6 +621,34 @@ export class ColumnDisplayComponent implements OnInit, OnDestroy {
} }
} }
private uploadFile(attachment: Attachment, response: ApiResponse) {
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: any) => {
input.value = '';
})
}
private updateTaskActivity(id: number) { private updateTaskActivity(id: number) {
this.viewTaskActivities = []; this.viewTaskActivities = [];

View File

@ -1 +1,17 @@
<tb-top-nav page-name="{{ pageName }}"></tb-top-nav> <tb-top-nav page-name="{{ pageName }}" [show-buttons]="false"></tb-top-nav>
<div class="file-viewer">
<div class="header">
{{ strings['attachment'] }}: {{ attachment?.filename }}
<span class="right">
<a [href]="fileUrl" download="{{ attachment?.filename }}">
{{strings['boards_taskDownload']}}
</a>
</span>
</div>
<div class="content" *ngIf="isLoaded">
<iframe seamless [src]="fileUrl"></iframe>
</div>
</div>

View File

@ -1,42 +1,78 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router';
import {
Title,
DomSanitizer,
SafeResourceUrl
} from '@angular/platform-browser';
import { StringsService, AuthService } from '../shared/services'; import { FileViewerService } from './file-viewer.service';
import { User } from '../shared/models'; import { User, Attachment } from '../shared/models';
import {
StringsService,
AuthService,
NotificationsService
} from '../shared/services';
@Component({ @Component({
selector: 'tb-file-viewer', selector: 'tb-file-viewer',
templateUrl: './file-viewer.component.html' templateUrl: './file-viewer.component.html'
}) })
export class FileViewerComponent implements OnInit, OnDestroy { export class FileViewerComponent implements OnInit, OnDestroy {
private subs: any[]; private subs: any[];
private fileHash: string;
public activeUser: User;
public pageName: string;
public strings: any; public strings: any;
public pageName: string;
public isLoaded: boolean;
public fileUrl: SafeResourceUrl;
constructor(public title: Title, public attachment: Attachment;
public auth: AuthService, public activeUser: User;
public stringsService: StringsService) {
constructor(private title: Title,
private active: ActivatedRoute,
private sanitizer: DomSanitizer,
public service: FileViewerService,
private notes: NotificationsService,
private auth: AuthService,
private stringsService: StringsService) {
title.setTitle('TaskBoard - File Viewer'); title.setTitle('TaskBoard - File Viewer');
this.isLoaded = false;
this.subs = []; this.subs = [];
let sub = stringsService.stringsChanged.subscribe(newStrings => { let sub = this.stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings; this.strings = newStrings;
this.pageName = this.strings.files;
this.title.setTitle(`TaskBoard - ${this.pageName}`);
}); });
this.subs.push(sub); this.subs.push(sub);
sub = auth.userChanged.subscribe((user: User) => { sub = this.auth.userChanged.subscribe((user: User) => {
this.activeUser = user; this.activeUser = user;
}); });
this.subs.push(sub); this.subs.push(sub);
sub = this.active.params.subscribe(params => {
this.fileHash = params.hash;
});
this.subs.push(sub);
} }
ngOnInit() { ngOnInit() {
this.pageName = this.strings.files; this.service.getAttachmentInfo(this.fileHash).subscribe(res => {
res.alerts.forEach(note => this.notes.add(note));
console.log(this.stringsService, this.activeUser, this.pageName); if (res.status === 'success') {
this.attachment = res.data[1];
const url = `./api/uploads/${this.attachment.diskfilename}`;
this.fileUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
this.isLoaded = true;
}
});
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { FileViewerComponent } from './file-viewer.component'; import { FileViewerComponent } from './file-viewer.component';
import { FileViewerService } from './file-viewer.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -15,6 +16,9 @@ import { FileViewerComponent } from './file-viewer.component';
declarations: [ declarations: [
FileViewerComponent FileViewerComponent
], ],
providers: [
FileViewerService
],
exports: [ exports: [
FileViewerComponent FileViewerComponent
] ]

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { ApiResponse } from '../shared/models';
@Injectable()
export class FileViewerService {
constructor(private http: HttpClient) {}
getAttachmentInfo(hash: string): Observable<ApiResponse> {
return this.http.get('api/attachments/hash/' + hash)
.pipe(
map((response: ApiResponse) => response),
catchError((err) => of(err.error as ApiResponse))
);
}
}