From 38a768892ea9fbcbc9e7fd4593529a4b9bf4fcd5 Mon Sep 17 00:00:00 2001 From: Ravi Theja Date: Fri, 23 Oct 2020 15:39:19 +0530 Subject: [PATCH 1/2] Change grouping strategy --- src/app/app.component.html | 33 +++++++--- src/app/app.component.ts | 66 +++++++++++-------- src/app/app.module.ts | 5 ++ src/app/models/index.ts | 2 +- src/app/models/task-board.ts | 6 -- src/app/models/task-drop-event.ts | 4 +- src/app/models/task-group.ts | 33 ++++++++++ src/app/services/task-manager.service.ts | 42 ++++++++---- src/app/task-board/task-board.component.html | 10 +-- src/app/task-board/task-board.component.scss | 6 ++ src/app/task-board/task-board.component.ts | 13 ++-- .../task-item-edit.component.html | 46 ++++++------- .../task-item-edit.component.ts | 20 +++--- src/app/task-item/task-item.component.html | 33 ++++++++-- src/app/task-item/task-item.component.ts | 1 + src/styles.scss | 10 +++ 16 files changed, 222 insertions(+), 108 deletions(-) delete mode 100644 src/app/models/task-board.ts create mode 100644 src/app/models/task-group.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 35fe5c5..4dceb42 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,7 +1,19 @@
-
+
+
+ + +
+
+
@@ -9,16 +21,17 @@
- - + - - -
- + + +
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c668c1d..feb7793 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,8 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BrowserTransferStateModule } from '@angular/platform-browser'; import { forkJoin, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TaskBoard, TaskDropEvent } from './models'; +import { TaskGroup } from './models'; import { User } from './models/user'; import { TaskManagerService } from './services/task-manager.service'; import { UsersService } from './services/users.service'; @@ -13,33 +12,58 @@ import { UsersService } from './services/users.service'; styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit, OnDestroy { - public users: User[]; + + private _users: User[]; + public searchTerm: string; public isLoading: boolean; public destroy$: Subject; - - public boards: TaskBoard[] = [{ - title: 'High Priority', - priority: 1, - },{ - title: 'Medium Priority', - priority: 2, + public selectedTaskGroup: TaskGroup; + + /** + * Predefined task grouping configurations + */ + public readonly taskGroups: TaskGroup[] = [{ + displayName: 'Priority', + propertyName: 'priority', + values: [...this.taskService.PRIORITIES.map(x => x.value), false], + displayLabels: [...this.taskService.PRIORITIES.map(x => x.label), 'No Priority'], },{ - title: 'Low Priority', - priority: 3, + displayName: 'Assigned To', + propertyName: 'assigned_to', + values: [], + displayLabels: [], + displayPictures: [], }]; + + public get users(): User[] { + return this._users; + } + + public set users(list: User[]) { + this._users = list; + const taskGroup = this.taskGroups.find(g => g.propertyName === 'assigned_to'); + if (taskGroup) { + taskGroup.values = [...list.map(x => x.id), false]; + taskGroup.displayLabels = [...list.map(x => x.name), 'Up for Grabs']; + taskGroup.displayPictures = [...list.map(x => x.picture), ''] + } + } constructor(private taskService: TaskManagerService, private userService: UsersService) { this.destroy$ = new Subject(); this.searchTerm = ''; - this.users = []; + this._users = []; this.isLoading = true; + this.selectedTaskGroup = this.taskGroups[0]; } public ngOnInit(): void { forkJoin([this.userService.fetchAll(), this.taskService.fetchAll()]).subscribe({ next: _ => { - // TODO: remove + this.userService.users$.pipe(takeUntil(this.destroy$)).subscribe({ + next: users => this.users = users, + }); }, error: error => { console.error('Initializion failed', error); }, complete: () => { @@ -48,18 +72,8 @@ export class AppComponent implements OnInit, OnDestroy { }); } - public handleDrop(event: TaskDropEvent) { - if (event.task.priority !== event.board.priority) { - console.log('Now this is interesting'); - event.task.priority = event.board.priority; - this.taskService.updateTask(event.task).subscribe({ - next: response => { - console.log(response); - } - }); - } else { - console.log('Dropped on the same board, how boring!'); - } + public updateGrouping(taskGroup: TaskGroup) { + this.selectedTaskGroup = taskGroup; } public ngOnDestroy(): void { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 62d56ac..b87604d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,10 @@ import { HttpClientModule } from '@angular/common/http' import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; + import { AppComponent } from './app.component'; import { TaskBoardComponent } from './task-board/task-board.component'; import { TaskItemComponent } from './task-item/task-item.component'; @@ -23,9 +26,11 @@ import { TaskItemEditComponent } from './task-item-edit/task-item-edit.component ], imports: [ BrowserModule, + BrowserAnimationsModule, HttpClientModule, FormsModule, ReactiveFormsModule, + BsDropdownModule.forRoot(), ], providers: [TaskFilterPipe], bootstrap: [AppComponent] diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 4283902..67359ad 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -1,4 +1,4 @@ -export { TaskBoard } from './task-board'; +export { TaskGroup } from './task-group'; export { TaskItem } from './task-item'; export { GenericResponse } from './generic-response'; export { TaskListResponse } from './task-list-response'; diff --git a/src/app/models/task-board.ts b/src/app/models/task-board.ts deleted file mode 100644 index e05d5bb..0000000 --- a/src/app/models/task-board.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TaskItem } from './task-item'; - -export interface TaskBoard { - title: string; - priority: number; -} \ No newline at end of file diff --git a/src/app/models/task-drop-event.ts b/src/app/models/task-drop-event.ts index 8d40ee0..555693b 100644 --- a/src/app/models/task-drop-event.ts +++ b/src/app/models/task-drop-event.ts @@ -1,4 +1,4 @@ -import { TaskBoard } from './task-board'; +import { TaskGroup } from './task-group'; import { TaskItem } from './task-item'; export interface TaskDropEvent { @@ -10,5 +10,5 @@ export interface TaskDropEvent { /** * The board it has been dropped to */ - board: TaskBoard; + board: TaskGroup; } \ No newline at end of file diff --git a/src/app/models/task-group.ts b/src/app/models/task-group.ts new file mode 100644 index 0000000..72b8a0f --- /dev/null +++ b/src/app/models/task-group.ts @@ -0,0 +1,33 @@ +/** + * Descibe a property based task grouping model + */ +export interface TaskGroup { + /** + * Name of this task grouping model + */ + displayName: string; + + /** + * The property name which is the basis + * for this grouping model + */ + propertyName: string; + + /** + * The list of values for `propertyName` the + * tasks should be divided into + */ + values: any[]; + + /** + * Display labels to be used as board titles. + * List should have the same number of elements as `values` + */ + displayLabels: string[]; + + /** + * Optional image urls. + * Should have the same number of elements as `values` + */ + displayPictures?: string[]; +} \ No newline at end of file diff --git a/src/app/services/task-manager.service.ts b/src/app/services/task-manager.service.ts index ca7cfc7..fb3e478 100644 --- a/src/app/services/task-manager.service.ts +++ b/src/app/services/task-manager.service.ts @@ -16,6 +16,20 @@ export class TaskManagerService { public tasks$: BehaviorSubject; public error$: BehaviorSubject; + public readonly PRIORITIES = [{ + value: 1, + label: 'High Priority', + styleClass: 'priority-high', + }, { + value: 2, + label: 'Medium Priority', + styleClass: 'priority-medium', + }, { + value: 3, + label: 'Low Priority', + styleClass: 'priority-low', + }]; + constructor(private httpClient: HttpClient) { this.API_URL = environment.apiUrl; this.API_KEY = environment.apiKey; @@ -55,20 +69,20 @@ export class TaskManagerService { }, })); } - + /** - * Create task with provided values - * @param task The task to be created - * - * @returns Observable with the response object - */ + * Create task with provided values + * @param task The task to be created + * + * @returns Observable with the response object + */ public createTask(task: TaskItem) { let requestBody = new FormData(); requestBody.set('message', task.message); requestBody.set('priority', task.priority?.toString() || ''); requestBody.set('due_date', DateUtils.toRequestFormat(task.due_date) || ''); requestBody.set('assigned_to', task.assigned_to?.toString() || ''); - + return this.httpClient.post(`${this.API_URL}/create`, requestBody, { headers: { AuthToken: this.API_KEY, @@ -84,13 +98,13 @@ export class TaskManagerService { } })); } - + /** - * Update existing task with provided values - * @param task - * - * @returns Observable of the response object - */ + * Update existing task with provided values + * @param task + * + * @returns Observable of the response object + */ public updateTask(task: TaskItem) { let requestBody = new FormData(); requestBody.set('message', task.message); @@ -98,7 +112,7 @@ export class TaskManagerService { requestBody.set('due_date', DateUtils.toRequestFormat(task.due_date) || ''); requestBody.set('assigned_to', task.assigned_to?.toString() || ''); requestBody.set('taskid', task.id); - + return this.httpClient.post(`${this.API_URL}/update`, requestBody, { headers: { AuthToken: this.API_KEY, diff --git a/src/app/task-board/task-board.component.html b/src/app/task-board/task-board.component.html index e0be6af..7dfd4ab 100644 --- a/src/app/task-board/task-board.component.html +++ b/src/app/task-board/task-board.component.html @@ -2,12 +2,13 @@
- - {{ filteredTasks.length | pad }} - + {{ title }} + + {{ filteredTasks.length | pad }} +
@@ -16,7 +17,8 @@ + [property]="property" + [value]="value"> diff --git a/src/app/task-board/task-board.component.scss b/src/app/task-board/task-board.component.scss index 519c476..c0268f1 100644 --- a/src/app/task-board/task-board.component.scss +++ b/src/app/task-board/task-board.component.scss @@ -16,4 +16,10 @@ } .task-board { box-shadow: 0 0 10px 0 #333; + + .display-picture { + width: 25px; + height: 25px; + border-radius: 25%; + } } \ No newline at end of file diff --git a/src/app/task-board/task-board.component.ts b/src/app/task-board/task-board.component.ts index 3e68189..a5d560a 100644 --- a/src/app/task-board/task-board.component.ts +++ b/src/app/task-board/task-board.component.ts @@ -13,7 +13,9 @@ import { TaskManagerService } from '../services/task-manager.service'; export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { @Input() title: string; - @Input() priority: number; + @Input() property: string; + @Input() value: string | number; + @Input() displayPicture: string; @Input() filterTerm: string; public destroy$: Subject; @@ -31,7 +33,10 @@ export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { public ngOnInit(): void { this.taskManager.tasks$.pipe(takeUntil(this.destroy$)).subscribe({ next: tasks => { - this.tasks = [...tasks.filter(task => task.priority === this.priority)]; + this.tasks = !!this.value + ? [...tasks.filter(task => task[this.property]==this.value)] + : [...tasks.filter(task => !task[this.property])]; + console.log('NgOnInit called', this.property, this.value, this.tasks); this.applyFilter(); } }); @@ -56,8 +61,8 @@ export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { task.due_date = new Date(task.due_date); task.created_on = new Date(task.created_on); console.log(task); - if (task.priority !== this.priority) { - task.priority = this.priority; + if (task[this.property] !== this.value) { + task[this.property] = this.value; this.taskManager.updateTask(task).subscribe(); } } catch(error) { diff --git a/src/app/task-item-edit/task-item-edit.component.html b/src/app/task-item-edit/task-item-edit.component.html index 501168e..c5c1ebf 100644 --- a/src/app/task-item-edit/task-item-edit.component.html +++ b/src/app/task-item-edit/task-item-edit.component.html @@ -3,37 +3,33 @@
-
- +
+ +
- +
+
+
-
- - + +
@@ -47,7 +43,7 @@
- +
diff --git a/src/app/task-item-edit/task-item-edit.component.ts b/src/app/task-item-edit/task-item-edit.component.ts index 368b209..eca6e4f 100644 --- a/src/app/task-item-edit/task-item-edit.component.ts +++ b/src/app/task-item-edit/task-item-edit.component.ts @@ -4,6 +4,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { TaskItem } from '../models'; import { User } from '../models/user'; +import { TaskManagerService } from '../services/task-manager.service'; import { UsersService } from '../services/users.service'; @Component({ @@ -13,7 +14,9 @@ import { UsersService } from '../services/users.service'; }) export class TaskItemEditComponent implements OnInit { - @Input() priority: number; + @Input() property: string; + @Input() value: any; + @Output() onCancel: EventEmitter; @Output() onSave: EventEmitter; @@ -21,16 +24,18 @@ export class TaskItemEditComponent implements OnInit { public users: User[]; public selectedUser: User; public editForm: FormGroup; + public readonly priorities = this.taskManager.PRIORITIES; - constructor(private userService: UsersService) { + constructor(private userService: UsersService, private taskManager: TaskManagerService) { this.onCancel = new EventEmitter(); this.onSave = new EventEmitter(); this.destroy$ = new Subject(); this.users = []; this.editForm = new FormGroup({ message: new FormControl('', [Validators.required]), - assigned_to: new FormControl(''), - due_date: new FormControl() + assigned_to: new FormControl('', [Validators.pattern(/^\d\d*$/)]), + due_date: new FormControl(), + priority: new FormControl(''), }); } @@ -38,13 +43,14 @@ export class TaskItemEditComponent implements OnInit { this.userService.users$.pipe(takeUntil(this.destroy$)).subscribe({ next: userList => this.users = userList, }); + this.editForm.get(this.property).setValue(this.value); } public save(): void { if (this.editForm.valid) { let user = this.users.find(u => u.id === this.editForm.get('assigned_to').value); this.onSave.emit({ - priority: this.priority, + priority: parseInt(this.editForm.get('priority').value), assigned_to: user && parseInt(user.id), created_on: new Date(), due_date: new Date(this.editForm.get('due_date').value), @@ -56,10 +62,6 @@ export class TaskItemEditComponent implements OnInit { } } - public edit(): void { - - } - public onCancelled() { this.onCancel.emit(); this.editForm.reset(); diff --git a/src/app/task-item/task-item.component.html b/src/app/task-item/task-item.component.html index 017d356..0779ce7 100644 --- a/src/app/task-item/task-item.component.html +++ b/src/app/task-item/task-item.component.html @@ -1,18 +1,34 @@ -
+
- {{ taskItem.message }} -
-
-
+
-
{{ taskItem.assigned_name }}
+
{{ taskItem.assigned_name }}
+
+
+
+
+
+ {{ taskItem.message }} +
+
+
+
+ + +
+ + + + {{ priority.label }} +
+
- {{ taskItem.due_date | dateFormat:false }} + {{ taskItem.due_date | dateFormat:false }}
@@ -28,6 +44,9 @@
+ + No Priority + Unassigned diff --git a/src/app/task-item/task-item.component.ts b/src/app/task-item/task-item.component.ts index b49cf11..8a2d658 100644 --- a/src/app/task-item/task-item.component.ts +++ b/src/app/task-item/task-item.component.ts @@ -17,6 +17,7 @@ export class TaskItemComponent implements OnInit, OnChanges { public isLoading: boolean; public isOverdue: boolean; public userDisplayPicture: string; + public readonly priorities = this.taskManager.PRIORITIES; constructor(private taskManager: TaskManagerService, private userService: UsersService) { this.onDelete = new EventEmitter(); diff --git a/src/styles.scss b/src/styles.scss index 72a1c11..c37c3a2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -56,4 +56,14 @@ body { .task-card:hover { box-shadow: 0 0 4px 0 #000; background-color: #fff; + border-left-width: 10px; +} +.priority-high { + color: deeppink; +} +.priority-medium { + color: orange; +} +.priority-low { + color: cornflowerblue; } \ No newline at end of file From 98540da105c4f768a31df0f09b111472b7186009 Mon Sep 17 00:00:00 2001 From: Ravi Theja Date: Fri, 23 Oct 2020 22:45:31 +0530 Subject: [PATCH 2/2] Add date range filter; AuthToken added via interceptor; minor bug fixes; --- package.json | 3 ++ src/app/app.component.html | 45 ++++++++++------ src/app/app.component.ts | 29 +++++++---- src/app/app.module.ts | 14 ++++- src/app/pipes/task-filter.pipe.ts | 13 ++++- src/app/services/auth-interceptor.ts | 15 ++++++ src/app/services/task-manager.service.ts | 36 ++++--------- src/app/services/users.service.ts | 14 ++--- src/app/task-board/task-board.component.html | 24 +++++++-- src/app/task-board/task-board.component.scss | 23 +++++++++ src/app/task-board/task-board.component.ts | 28 ++++++++-- .../task-item-edit.component.html | 10 +++- .../task-item-edit.component.ts | 19 +++++-- src/app/task-item/task-item.component.html | 21 ++++---- src/app/task-item/task-item.component.ts | 8 +-- src/app/utils/date-utils.ts | 25 ++++++++- src/index.html | 1 + src/styles.scss | 51 +++++++++++++++++-- 18 files changed, 283 insertions(+), 96 deletions(-) create mode 100644 src/app/services/auth-interceptor.ts diff --git a/package.json b/package.json index 5f95aee..2a10210 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "mini-task-manager", "version": "0.0.0", + "repository": { + "url": "https://github.com/cyberpirate92/mini-task-manager" + }, "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/app.component.html b/src/app/app.component.html index 4dceb42..6f783d7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,33 +1,48 @@
-
-
+
+
-
-
- +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
-
-
+
+
- +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index feb7793..5dc8df1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,22 +12,23 @@ import { UsersService } from './services/users.service'; styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit, OnDestroy { - + private _users: User[]; - + public searchTerm: string; public isLoading: boolean; public destroy$: Subject; public selectedTaskGroup: TaskGroup; - + public filterDateRange: Date[]; + /** - * Predefined task grouping configurations - */ + * Predefined task grouping configurations + */ public readonly taskGroups: TaskGroup[] = [{ displayName: 'Priority', propertyName: 'priority', - values: [...this.taskService.PRIORITIES.map(x => x.value), false], - displayLabels: [...this.taskService.PRIORITIES.map(x => x.label), 'No Priority'], + values: this.taskService.PRIORITIES.map(x => x.value), + displayLabels: this.taskService.PRIORITIES.map(x => x.label), },{ displayName: 'Assigned To', propertyName: 'assigned_to', @@ -35,11 +36,11 @@ export class AppComponent implements OnInit, OnDestroy { displayLabels: [], displayPictures: [], }]; - + public get users(): User[] { return this._users; } - + public set users(list: User[]) { this._users = list; const taskGroup = this.taskGroups.find(g => g.propertyName === 'assigned_to'); @@ -56,6 +57,7 @@ export class AppComponent implements OnInit, OnDestroy { this._users = []; this.isLoading = true; this.selectedTaskGroup = this.taskGroups[0]; + this.filterDateRange = []; } public ngOnInit(): void { @@ -72,6 +74,15 @@ export class AppComponent implements OnInit, OnDestroy { }); } + public onDateRangeChange(change: Date[]) { + if (change && typeof change === 'object' && change instanceof Array) { + this.filterDateRange = [...change]; + console.log(this.filterDateRange); + } else { + this.filterDateRange = []; + } + } + public updateGrouping(taskGroup: TaskGroup) { this.selectedTaskGroup = taskGroup; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b87604d..97faeca 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,9 +1,10 @@ -import { HttpClientModule } from '@angular/common/http' +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http' import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { AppComponent } from './app.component'; import { TaskBoardComponent } from './task-board/task-board.component'; @@ -13,6 +14,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TaskFilterPipe } from './pipes/task-filter.pipe'; import { PadPipe } from './pipes/pad.pipe'; import { TaskItemEditComponent } from './task-item-edit/task-item-edit.component'; +import { AuthInterceptor } from './services/auth-interceptor'; @NgModule({ declarations: [ @@ -31,8 +33,16 @@ import { TaskItemEditComponent } from './task-item-edit/task-item-edit.component FormsModule, ReactiveFormsModule, BsDropdownModule.forRoot(), + BsDatepickerModule.forRoot(), + ], + providers: [ + TaskFilterPipe, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + } ], - providers: [TaskFilterPipe], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/pipes/task-filter.pipe.ts b/src/app/pipes/task-filter.pipe.ts index 68c810c..341ef49 100644 --- a/src/app/pipes/task-filter.pipe.ts +++ b/src/app/pipes/task-filter.pipe.ts @@ -1,13 +1,22 @@ import { Pipe } from "@angular/core"; import { TaskItem } from '../models'; +import { DateUtils } from '../utils/date-utils'; @Pipe({ name: 'taskFilter' }) export class TaskFilterPipe { - public transform(tasks: TaskItem[], filterString: string): TaskItem[] { + public transform(tasks: TaskItem[], filterString: string, dateRange: Date[] = []): TaskItem[] { + const isValidDateRange = dateRange && dateRange.length >= 2 && DateUtils.isDate(dateRange[0]) && DateUtils.isDate(dateRange[1]); + console.log('is-valid-daterange', dateRange, isValidDateRange); + if (isValidDateRange) { + tasks = tasks.filter(x => x.due_date >= dateRange[0] && x.due_date <= dateRange[1]); + } filterString = filterString.trim().toLowerCase(); - return tasks.filter(task => this.combineStrings(task).indexOf(filterString) >= 0); + if (filterString) { + tasks = tasks.filter(task => this.combineStrings(task).indexOf(filterString) >= 0); + } + return tasks; } /** diff --git a/src/app/services/auth-interceptor.ts b/src/app/services/auth-interceptor.ts new file mode 100644 index 0000000..a56fc88 --- /dev/null +++ b/src/app/services/auth-interceptor.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { + return next.handle(httpRequest.clone({ + setHeaders: { + AuthToken: environment.apiKey, + } + }));; + } +} \ No newline at end of file diff --git a/src/app/services/task-manager.service.ts b/src/app/services/task-manager.service.ts index fb3e478..370c3ce 100644 --- a/src/app/services/task-manager.service.ts +++ b/src/app/services/task-manager.service.ts @@ -1,6 +1,6 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { tap } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { CreateTaskResponse, GenericResponse, TaskItem, TaskListResponse } from '../models'; @@ -11,28 +11,26 @@ import { DateUtils } from '../utils/date-utils'; }) export class TaskManagerService { private API_URL: string; - private API_KEY: string; public tasks$: BehaviorSubject; public error$: BehaviorSubject; public readonly PRIORITIES = [{ value: 1, - label: 'High Priority', - styleClass: 'priority-high', + label: 'Low Priority', + styleClass: 'priority-low', }, { value: 2, label: 'Medium Priority', styleClass: 'priority-medium', }, { value: 3, - label: 'Low Priority', - styleClass: 'priority-low', + label: 'High Priority', + styleClass: 'priority-high', }]; constructor(private httpClient: HttpClient) { this.API_URL = environment.apiUrl; - this.API_KEY = environment.apiKey; this.tasks$ = new BehaviorSubject([]); this.error$ = new BehaviorSubject(""); @@ -45,11 +43,7 @@ export class TaskManagerService { * @returns Observable of the response object */ public fetchAll() { - return this.httpClient.get(`${this.API_URL}/list`, { - headers: { - AuthToken: this.API_KEY, - } - }).pipe(tap({ + return this.httpClient.get(`${this.API_URL}/list`).pipe(tap({ next: response => { if (response.status === 'success') { this.tasks$.next(response.tasks.map(t => { @@ -83,11 +77,7 @@ export class TaskManagerService { requestBody.set('due_date', DateUtils.toRequestFormat(task.due_date) || ''); requestBody.set('assigned_to', task.assigned_to?.toString() || ''); - return this.httpClient.post(`${this.API_URL}/create`, requestBody, { - headers: { - AuthToken: this.API_KEY, - } - }).pipe(tap({ + return this.httpClient.post(`${this.API_URL}/create`, requestBody).pipe(tap({ next: response => { if (response.status === 'success') { task.id = response.taskid; @@ -113,11 +103,7 @@ export class TaskManagerService { requestBody.set('assigned_to', task.assigned_to?.toString() || ''); requestBody.set('taskid', task.id); - return this.httpClient.post(`${this.API_URL}/update`, requestBody, { - headers: { - AuthToken: this.API_KEY, - } - }).pipe(tap({ + return this.httpClient.post(`${this.API_URL}/update`, requestBody).pipe(tap({ next: response => { if (response.status === 'success') { const taskList = [...this.tasks$.getValue()]; @@ -145,11 +131,7 @@ export class TaskManagerService { public deleteTask(taskId: string) { let requestBody = new FormData(); requestBody.set('taskid', taskId); - return this.httpClient.post(`${this.API_URL}/delete`, requestBody, { - headers: { - AuthToken: this.API_KEY, - } - }).pipe(tap({ + return this.httpClient.post(`${this.API_URL}/delete`, requestBody).pipe(tap({ next: response => { if (response.status === 'success') { this.tasks$.next(this.tasks$.getValue().filter(task => task.id !== taskId)); diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts index 5bca613..908c62c 100644 --- a/src/app/services/users.service.ts +++ b/src/app/services/users.service.ts @@ -25,11 +25,7 @@ export class UsersService { * @returns observable of the response object */ public fetchAll() { - return this.httpClient.get(`${this.API_URL}/listusers`, { - headers: { - AuthToken: environment.apiKey, - } - }).pipe(tap({ + return this.httpClient.get(`${this.API_URL}/listusers`).pipe(tap({ next: response => { if (response.status !== 'success') { console.error(response.error || 'Fetching user list failed: Unknown error'); @@ -43,11 +39,11 @@ export class UsersService { } })); } - + /** - * Fetch user by User ID from cache - * @param id User ID - */ + * Fetch user by User ID from cache + * @param id User ID + */ public getUserById(id: number): User { return id ? this.users$.getValue().find(x => x.id == id.toString()) : null; } diff --git a/src/app/task-board/task-board.component.html b/src/app/task-board/task-board.component.html index 7dfd4ab..422dafb 100644 --- a/src/app/task-board/task-board.component.html +++ b/src/app/task-board/task-board.component.html @@ -1,19 +1,22 @@
-
+
{{ title }} - + {{ filteredTasks.length | pad }}
-
+
-
\ No newline at end of file +
+ +
+

+ +

+
+ No tasks here +
+
+ Click on the icon to add tasks or drag one from another board +
+
+
\ No newline at end of file diff --git a/src/app/task-board/task-board.component.scss b/src/app/task-board/task-board.component.scss index c0268f1..a14da8e 100644 --- a/src/app/task-board/task-board.component.scss +++ b/src/app/task-board/task-board.component.scss @@ -16,10 +16,33 @@ } .task-board { box-shadow: 0 0 10px 0 #333; + transition: 0.2s all ease-in-out; .display-picture { width: 25px; height: 25px; border-radius: 25%; } + .drag-overlay { + position: absolute; + visibility: hidden; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; + background-color: rgba(0, 0, 0, .5); + color: #f5f5f5; + z-index: 2147483647; + text-align: center; + font-size: 1.25rem; + } + .item-count { + line-height: 1.2; + } +} +.task-board.drag-highlight { + .drag-overlay { + visibility: visible; + } } \ No newline at end of file diff --git a/src/app/task-board/task-board.component.ts b/src/app/task-board/task-board.component.ts index a5d560a..2d9ccb6 100644 --- a/src/app/task-board/task-board.component.ts +++ b/src/app/task-board/task-board.component.ts @@ -4,6 +4,7 @@ import { takeUntil } from 'rxjs/operators'; import { TaskItem } from '../models'; import { TaskFilterPipe } from '../pipes/task-filter.pipe'; import { TaskManagerService } from '../services/task-manager.service'; +import { UsersService } from '../services/users.service'; @Component({ selector: 'app-task-board', @@ -17,17 +18,20 @@ export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { @Input() value: string | number; @Input() displayPicture: string; @Input() filterTerm: string; + @Input() filterDateRange: Date[]; public destroy$: Subject; public tasks: TaskItem[]; public filteredTasks: TaskItem[]; public showCreateTaskCard: boolean; + public hasDragOver: boolean; - constructor(private taskFilterPipe: TaskFilterPipe, private taskManager: TaskManagerService) { + constructor(private taskFilterPipe: TaskFilterPipe, private taskManager: TaskManagerService, private userService: UsersService) { this.filteredTasks = []; this.destroy$ = new Subject(); this.tasks = []; this.showCreateTaskCard = false; + this.hasDragOver = false; } public ngOnInit(): void { @@ -43,19 +47,21 @@ export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { } public ngOnChanges(changes: SimpleChanges): void { - if (changes.filterTerm) { + if (changes.filterTerm || changes.filterDateRange) { + console.log(changes); this.applyFilter(); } } public applyFilter(): void { - this.filteredTasks = this.filterTerm - ? this.taskFilterPipe.transform(this.tasks, this.filterTerm) - : [...this.tasks]; + this.filteredTasks = this.taskFilterPipe.transform(this.tasks, this.filterTerm, this.filterDateRange); } public onDropped(event: DragEvent) { console.log(event); + if (this.hasDragOver) { + this.hasDragOver = false; + } try { let task = JSON.parse(event.dataTransfer.getData('task')) as TaskItem; task.due_date = new Date(task.due_date); @@ -63,12 +69,24 @@ export class TaskBoardComponent implements OnInit, OnChanges, OnDestroy { console.log(task); if (task[this.property] !== this.value) { task[this.property] = this.value; + if (task.assigned_to) { + task.assigned_name = this.userService.getUserById(task.assigned_to).name; + } this.taskManager.updateTask(task).subscribe(); } } catch(error) { console.error(error); } } + + public onDragOver(event: DragEvent) { + event.preventDefault(); + this.hasDragOver = true; + } + + public onDragLeave(event: DragEvent) { + this.hasDragOver = false; + } public newTask(): void { this.showCreateTaskCard = true; diff --git a/src/app/task-item-edit/task-item-edit.component.html b/src/app/task-item-edit/task-item-edit.component.html index c5c1ebf..d7174e8 100644 --- a/src/app/task-item-edit/task-item-edit.component.html +++ b/src/app/task-item-edit/task-item-edit.component.html @@ -26,7 +26,6 @@
+
+
+
+ +
+
+ +
diff --git a/src/app/task-item-edit/task-item-edit.component.ts b/src/app/task-item-edit/task-item-edit.component.ts index eca6e4f..517ccd1 100644 --- a/src/app/task-item-edit/task-item-edit.component.ts +++ b/src/app/task-item-edit/task-item-edit.component.ts @@ -17,6 +17,8 @@ export class TaskItemEditComponent implements OnInit { @Input() property: string; @Input() value: any; + @Input() taskItem: TaskItem; + @Output() onCancel: EventEmitter; @Output() onSave: EventEmitter; @@ -35,7 +37,7 @@ export class TaskItemEditComponent implements OnInit { message: new FormControl('', [Validators.required]), assigned_to: new FormControl('', [Validators.pattern(/^\d\d*$/)]), due_date: new FormControl(), - priority: new FormControl(''), + priority: new FormControl(this.priorities[0].value.toString()), }); } @@ -43,20 +45,29 @@ export class TaskItemEditComponent implements OnInit { this.userService.users$.pipe(takeUntil(this.destroy$)).subscribe({ next: userList => this.users = userList, }); - this.editForm.get(this.property).setValue(this.value); + if (this.property && this.value) { + this.editForm.get(this.property).setValue(this.value); + } + if (this.taskItem) { + this.editForm.get('message').setValue(this.taskItem.message || ''); + this.editForm.get('priority').setValue(this.taskItem.priority || ''); + this.editForm.get('assigned_to').setValue(this.taskItem.assigned_to || ''); + this.editForm.get('due_date').setValue(this.taskItem.due_date || ''); + } } public save(): void { if (this.editForm.valid) { let user = this.users.find(u => u.id === this.editForm.get('assigned_to').value); + let dueDate = this.editForm.get('due_date').value; this.onSave.emit({ priority: parseInt(this.editForm.get('priority').value), assigned_to: user && parseInt(user.id), created_on: new Date(), - due_date: new Date(this.editForm.get('due_date').value), + due_date: dueDate ? new Date(dueDate) : null, message: this.editForm.get('message').value, assigned_name: user && user.name, - id: null, + id: this.taskItem?.id || null, }); this.editForm.reset(); } diff --git a/src/app/task-item/task-item.component.html b/src/app/task-item/task-item.component.html index 0779ce7..4f9f6c1 100644 --- a/src/app/task-item/task-item.component.html +++ b/src/app/task-item/task-item.component.html @@ -1,4 +1,6 @@ -
+ + +
@@ -13,6 +15,13 @@
{{ taskItem.message }}
+
+
+ Debug +
{{ taskItem | json }}
+
+
+
@@ -32,14 +41,8 @@
-
- +
@@ -51,7 +54,7 @@ Unassigned - No Due Date + No Due Date
diff --git a/src/app/task-item/task-item.component.ts b/src/app/task-item/task-item.component.ts index 8a2d658..894ef73 100644 --- a/src/app/task-item/task-item.component.ts +++ b/src/app/task-item/task-item.component.ts @@ -14,6 +14,7 @@ export class TaskItemComponent implements OnInit, OnChanges { @Input() taskItem: TaskItem; @Output() onDelete: EventEmitter; + public inEditMode: boolean; public isLoading: boolean; public isOverdue: boolean; public userDisplayPicture: string; @@ -23,6 +24,7 @@ export class TaskItemComponent implements OnInit, OnChanges { this.onDelete = new EventEmitter(); this.isLoading = false; this.isOverdue = false; + this.inEditMode = false; this.userDisplayPicture = environment.defaultDisplayPicture; } @@ -36,12 +38,12 @@ export class TaskItemComponent implements OnInit, OnChanges { public refresh(): void { const now = new Date(); - this.isOverdue = this.taskItem.due_date < now; + this.isOverdue = this.taskItem.due_date && this.taskItem.due_date < now; this.userDisplayPicture = this.userService.getUserById(this.taskItem?.assigned_to)?.picture; } - public editSelf(): void { - // TODO: Implementation + public updateSelf(updatedTaskItem: TaskItem): void { + this.taskManager.updateTask(updatedTaskItem).subscribe(); } public deleteSelf(): void { diff --git a/src/app/utils/date-utils.ts b/src/app/utils/date-utils.ts index 603ab57..77b9004 100644 --- a/src/app/utils/date-utils.ts +++ b/src/app/utils/date-utils.ts @@ -11,7 +11,7 @@ export class DateUtils { * Check if value is a date object */ public static isDate(value: any): boolean { - return value && typeof value === 'object' && value instanceof Date; + return !!(value && typeof value === 'object' && value instanceof Date); } /** @@ -48,7 +48,7 @@ export class DateUtils { * @param date The date object to convert * @param shouldIncludeTime Should the formatted string contain time? * - * @returns Date in Display format + * @returns formatted date string */ public static toDisplayFormat(date: Date, shouldIncludeTime = true): string { if (!this.isDate(date)) { @@ -58,4 +58,25 @@ export class DateUtils { const displayTime = `${date.getHours()}:${date.getMinutes()}`; return shouldIncludeTime ? `${displayDate} ${displayTime}` : displayDate; } + + /** + * Convert date object to standard input[type='date'] format yyyy-mm-dd + * @param date The date object to transform + * + * @returns formatted date string + */ + public static toDateInputFormat(date: Date): string { + if (!this.isDate(date)) { + return ''; + } + return `${date.getFullYear()}-${StringUtils.pad(date.getMonth()+1)}-${StringUtils.pad(date.getDate())}`; + } + + /** + * Convert date string from + * @param dateString + */ + public static fromDateInputFormat(dateString: string): Date { + return new Date(); + } } \ No newline at end of file diff --git a/src/index.html b/src/index.html index e2c0da7..6133189 100644 --- a/src/index.html +++ b/src/index.html @@ -10,6 +10,7 @@ + diff --git a/src/styles.scss b/src/styles.scss index c37c3a2..e3f04bf 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,22 @@ /* You can add global styles to this file, and also import other style files */ +$breakpoints: ( +xs: 576px, +sm: 768px, +md: 992px, +lg: 1200px +); + +@mixin respond-above($breakpoint) { + @if map-has-key($breakpoints, $breakpoint) { + $breakpoint-value: map-get($breakpoints, $breakpoint); + @media (min-width: $breakpoint-value) { + @content; + } + } @else { + @warn 'Invalid breakpoint: #{$breakpoint}.'; + } +} + body, html { height: 100vh; font-size: 13px; @@ -25,6 +43,20 @@ body { .font-weight-semi-bold { font-weight: 600; } +.empty-state { + + margin-top: 1rem; + margin-bottom: 1rem; + + .message.message-primary { + margin-top: 0.5rem; + font-size: 1.2rem; + } + .message.message-secondary { + font-size: 1.1rem; + margin-top: 0.25rem; + } +} .task-card { box-shadow: 0 0 2px 0 #444; transition: all .2s ease-in-out; @@ -38,11 +70,11 @@ body { height: 20px; border-radius: 50%; } - + textarea, input[type='text'], input[type='date'], select { font-size: 1.2rem; } - + button { font-size: 1.1rem; } @@ -66,4 +98,17 @@ body { } .priority-low { color: cornflowerblue; -} \ No newline at end of file +} +.clickable { + cursor: pointer; +} +@include respond-above(sm) { + .restrict-width-md { + max-width: 25%; + } +} +// .board-container { + // display: flex; + // overflow-x: auto; + // flex-wrap: nowrap; + // } \ No newline at end of file