WIP - New context menu impl

This commit is contained in:
Matthew Ross 2018-06-30 08:33:22 -04:00
parent c58e54b176
commit 044b72c820
20 changed files with 1363 additions and 463 deletions

View File

@ -20,5 +20,5 @@ after_success:
- touch fakeconfig - touch fakeconfig
- echo "OAUTH_ACCESS_TOKEN=$OAUTH_ACCESS_TOKEN" > fakeconfig - echo "OAUTH_ACCESS_TOKEN=$OAUTH_ACCESS_TOKEN" > fakeconfig
- ./dropbox_uploader.sh -f fakeconfig upload coverage/api coverage-$(php version.php)/ - ./dropbox_uploader.sh -f fakeconfig upload coverage/api coverage-$(php version.php)/
- ./dropbox_uploader.sh -f fakeconfig upload coverage/app coverage-$(php version.php)/ - ./dropbox_uploader.sh -f fakeconfig upload coverage/app/lcov-report coverage-$(php version.php)/

View File

@ -155,11 +155,11 @@ Because I like seeing the numbers.
Language | Files | Blank | Comment | Code Language | Files | Blank | Comment | Code
-------------|--------:|---------:|--------:|---------: -------------|--------:|---------:|--------:|---------:
TypeScript | 64 | 925 | 86 | 4091 TypeScript | 63 | 904 | 78 | 3932
PHP | 19 | 624 | 27 | 1997 PHP | 19 | 624 | 27 | 1997
HTML | 19 | 152 | 1 | 1422 HTML | 20 | 159 | 0 | 1479
SASS | 14 | 269 | 12 | 1215 SASS | 14 | 269 | 12 | 1211
__SUM:__ | __116__ | __1970__ | __126__ | __8725__ __SUM:__ | __116__ | __1956__ | __117__ | __8619__
Command: `cloc --exclude-dir=vendor --exclude-ext=json src/` Command: `cloc --exclude-dir=vendor --exclude-ext=json src/`

1269
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,15 +40,15 @@
"postinstall": "cd src/api/ && composer update && composer install --optimize-autoloader && cd ../../" "postinstall": "cd src/api/ && composer update && composer install --optimize-autoloader && cd ../../"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^6.0.4", "@angular/animations": "^6.0.7",
"@angular/common": "^6.0.4", "@angular/common": "^6.0.7",
"@angular/compiler": "^6.0.4", "@angular/compiler": "^6.0.7",
"@angular/core": "^6.0.4", "@angular/core": "^6.0.7",
"@angular/forms": "^6.0.4", "@angular/forms": "^6.0.7",
"@angular/http": "^6.0.4", "@angular/http": "^6.0.7",
"@angular/platform-browser": "^6.0.4", "@angular/platform-browser": "^6.0.7",
"@angular/platform-browser-dynamic": "^6.0.4", "@angular/platform-browser-dynamic": "^6.0.7",
"@angular/router": "^6.0.4", "@angular/router": "^6.0.7",
"chartist": "^0.11.0", "chartist": "^0.11.0",
"chartist-plugin-tooltips": "^0.0.17", "chartist-plugin-tooltips": "^0.0.17",
"classlist.js": "^1.1.20150312", "classlist.js": "^1.1.20150312",
@ -58,25 +58,25 @@
"marked": "^0.4.0", "marked": "^0.4.0",
"ng2-dragula": "^1.5.0", "ng2-dragula": "^1.5.0",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"rxjs": "^6.2.0", "rxjs": "^6.2.1",
"scss-base": "^1.4.0", "scss-base": "^1.4.0",
"zone.js": "^0.8.26" "zone.js": "^0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.6.5", "@angular-devkit/build-angular": "~0.6.8",
"@angular/cli": "^6.0.8", "@angular/cli": "^6.0.8",
"@angular/compiler-cli": "^6.0.4", "@angular/compiler-cli": "^6.0.7",
"@angular/language-service": "^6.0.4", "@angular/language-service": "^6.0.7",
"@types/jasmine": "~2.8.7", "@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "~10.1.4", "@types/node": "~10.5.0",
"bourbon": "5.0.0", "bourbon": "5.0.1",
"bourbon-neat": "1.9.0", "bourbon-neat": "1.9.0",
"codelyzer": "^4.3.0", "codelyzer": "^4.4.2",
"jasmine": "^3.1.0", "jasmine": "^3.1.0",
"jasmine-core": "^3.1.0", "jasmine-core": "^3.1.0",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.2", "karma": "~2.0.4",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.1", "karma-coverage-istanbul-reporter": "^2.0.1",
"karma-jasmine": "~1.1.2", "karma-jasmine": "~1.1.2",
@ -85,7 +85,7 @@
"nodemon": "^1.17.5", "nodemon": "^1.17.5",
"npm-run-all": "^4.1.3", "npm-run-all": "^4.1.3",
"protractor": "~5.3.2", "protractor": "~5.3.2",
"ts-node": "~6.0.5", "ts-node": "~7.0.0",
"tslint": "~5.10.0", "tslint": "~5.10.0",
"typescript": "^2.7.2" "typescript": "^2.7.2"
} }

10
src/api/composer.lock generated
View File

@ -258,16 +258,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.8.0", "version": "1.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6" "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/478465659fd987669df0bd8a9bf22a8710e5f1b6", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6", "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -302,7 +302,7 @@
"object", "object",
"object graph" "object graph"
], ],
"time": "2018-05-29T17:25:09+00:00" "time": "2018-06-11T23:09:50+00:00"
}, },
{ {
"name": "myclabs/php-enum", "name": "myclabs/php-enum",

View File

@ -26,7 +26,7 @@ export class ApiInterceptor implements HttpInterceptor {
const headers = { const headers = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
const token = localStorage.getItem(this.JWT_KEY); const token = sessionStorage.getItem(this.JWT_KEY);
if (token !== null) { if (token !== null) {
headers['Authorization'] = token; headers['Authorization'] = token;
@ -45,14 +45,14 @@ export class ApiInterceptor implements HttpInterceptor {
if ((evt.status === 401 || evt.status === 400) && if ((evt.status === 401 || evt.status === 400) &&
(evt.url + '').indexOf('login') === -1) { (evt.url + '').indexOf('login') === -1) {
localStorage.removeItem(this.JWT_KEY); sessionStorage.removeItem(this.JWT_KEY);
this.router.navigate(['']); this.router.navigate(['']);
return; return;
} }
const response: ApiResponse = evt.body; const response: ApiResponse = evt.body;
if (response.data) { if (response.data) {
localStorage.setItem(this.JWT_KEY, response.data[0]); sessionStorage.setItem(this.JWT_KEY, response.data[0]);
} }
}) })
); );

View File

@ -76,12 +76,7 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
this.pageName = this.strings.boards; this.pageName = this.strings.boards;
sub = this.boardService.getBoards().subscribe((response: ApiResponse) => { this.updateBoards();
this.boards = [];
this.updateBoardsList(response.data[1]);
this.loading = false;
});
this.subs.push(sub);
sub = boardService.activeBoardChanged.subscribe((board: Board) => { sub = boardService.activeBoardChanged.subscribe((board: Board) => {
if (!board) { if (!board) {
@ -137,7 +132,14 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
} }
}); });
this.dragula.dropModel.subscribe((value: any) => { let makeCall = false;
this.dragula.dropModel.subscribe(async (value: any) => {
makeCall = !makeCall;
if (!makeCall) {
return;
}
let taskId = +value[1].id, let taskId = +value[1].id,
toColumnId = +value[2].parentNode.id, toColumnId = +value[2].parentNode.id,
fromColumnId = +value[3].parentNode.id; fromColumnId = +value[3].parentNode.id;
@ -146,6 +148,8 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
fromColumnId = -1; fromColumnId = -1;
} }
let updateList = [];
this.activeBoard.columns.forEach(column => { this.activeBoard.columns.forEach(column => {
if (column.id === toColumnId || column.id === fromColumnId) { if (column.id === toColumnId || column.id === fromColumnId) {
let position = 1, let position = 1,
@ -158,10 +162,17 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
position++; position++;
}); });
let oneOff = this.boardService.updateColumn(column).subscribe(); updateList.push(column);
oneOff.unsubscribe();
} }
}); });
const update = async (column) => {
this.boardService.updateColumn(column).subscribe();
}
for (let i = 0, len = updateList.length; i < len; ++i) {
await update(updateList[i]);
}
}); });
} }
@ -239,6 +250,13 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
return false; return false;
} }
private updateBoards(): void {
this.boardService.getBoards().subscribe((response: ApiResponse) => {
this.boards = [];
this.updateBoardsList(response.data[1]);
});
}
private updateBoardsList(boards: Array<any>): void { private updateBoardsList(boards: Array<any>): void {
let activeBoards: Array<Board> = []; let activeBoards: Array<Board> = [];

View File

@ -1,6 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators';
@ -13,13 +15,42 @@ import {
User User
} from '../shared/models'; } from '../shared/models';
interface MarkedReturn {
html: string;
counts: any;
}
@Injectable() @Injectable()
export class BoardService { export class BoardService {
private checkCounts = {
total: 0,
complete: 0
}
private activeBoard = new BehaviorSubject<Board>(null); private activeBoard = new BehaviorSubject<Board>(null);
private defaultCallback = (err: any, text: string) => {
console.log('default', err, text);
return text;
};
public activeBoardChanged = this.activeBoard.asObservable(); public activeBoardChanged = this.activeBoard.asObservable();
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
this.initMarked();
}
convertMarkdown(markdown: string, callback = this.defaultCallback, doCount = false): MarkedReturn {
this.checkCounts.total = 0;
this.checkCounts.complete = 0;
let retVal: MarkedReturn = { html: '', counts: {} };
retVal.html = marked(markdown, callback);
if (doCount) {
retVal.counts = this.checkCounts;
}
return retVal;
} }
updateActiveBoard(board: Board): void { updateActiveBoard(board: Board): void {
@ -124,5 +155,42 @@ export class BoardService {
boardData.ownIssuetracker, boardData.ownIssuetracker,
boardData.sharedUser); boardData.sharedUser);
} }
private initMarked(): void {
let renderer = new marked.Renderer();
renderer.checkbox = isChecked => {
let text = '<i class="icon icon-check' + (isChecked ? '' : '-empty') + '"></i>';
this.checkCounts.total += 1;
if (isChecked) {
this.checkCounts.complete += 1;
}
return text;
};
renderer.link = (href, title, text) => {
let out = '<a href="' + href + '"';
if (title) {
out += ' title="' + title + '"';
}
out += ' target="tb_external" rel="noreferrer">' + text + '</a>';
return out;
};
marked.setOptions({
renderer,
smartypants: true,
highlight: code => {
return hljs.highlightAuto(code).value;
}
});
}
} }

View File

@ -87,7 +87,9 @@
</div> </div>
<tb-context-menu> <tb-context-menu>
<tb-context-menu-item>Column Test</tb-context-menu-item> <tb-context-menu-item (click)="showModal()">
{{ strings['boards_addTask'] }}
</tb-context-menu-item>
</tb-context-menu> </tb-context-menu>
<tb-modal *ngIf="activeBoard && columnData" <tb-modal *ngIf="activeBoard && columnData"
@ -108,7 +110,7 @@
</div> </div>
<div class="description" *ngIf="viewModalProps.description.length" <div class="description" *ngIf="viewModalProps.description.length"
[innerHtml]="getTaskDescription()"> [innerHtml]="viewModalProps.html">
</div> </div>
<div class="stats"> <div class="stats">

View File

@ -9,9 +9,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import { import {
ApiResponse, ApiResponse,
ActivitySimple, ActivitySimple,
@ -74,7 +71,8 @@ export class ColumnDisplay implements OnInit, OnDestroy {
@Input('column') columnData: Column; @Input('column') columnData: Column;
@Input('boards') boards: Array<Board>; @Input('boards') boards: Array<Board>;
@Output('on-update-boards') onUpdateBoards: EventEmitter<any> = new EventEmitter<any>(); @Output('on-update-boards')
onUpdateBoards: EventEmitter<any> = new EventEmitter<any>();
constructor(private elRef: ElementRef, constructor(private elRef: ElementRef,
private auth: AuthService, private auth: AuthService,
@ -99,11 +97,6 @@ export class ColumnDisplay implements OnInit, OnDestroy {
let sub = stringsService.stringsChanged.subscribe(newStrings => { let sub = stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings; this.strings = newStrings;
// this.contextMenuItems = [
// new ContextMenuItem(this.strings.boards_addTask,
// this.getShowModalFunction())
// ];
}); });
this.subs.push(sub); this.subs.push(sub);
@ -477,6 +470,11 @@ export class ColumnDisplay implements OnInit, OnDestroy {
updatedTask.ownAttachment, updatedTask.ownAttachment,
updatedTask.sharedUser, updatedTask.sharedUser,
updatedTask.sharedCategory); updatedTask.sharedCategory);
const data = this.boardService.convertMarkdown(task.description,
(_, text) => { return text; }, true);
task.html = data.html;
return task; return task;
} }
@ -536,14 +534,9 @@ export class ColumnDisplay implements OnInit, OnDestroy {
return text; return text;
} }
private getTaskDescription() {
let html = marked(this.viewModalProps.description, this.markedCallback);
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
}
private getComment(text: string) { private getComment(text: string) {
let html = marked(text, this.markedCallback); let data = this.boardService.convertMarkdown(text, this.markedCallback);
return this.sanitizer.bypassSecurityTrustHtml(html); return this.sanitizer.bypassSecurityTrustHtml(data.html);
} }
private getUserName(userId: number) { private getUserName(userId: number) {
@ -579,12 +572,7 @@ export class ColumnDisplay implements OnInit, OnDestroy {
}); });
this.newComment = ''; this.newComment = '';
this.viewModalProps = new Task(viewTask.id, viewTask.title, this.viewModalProps = this.convertToTask(viewTask);
viewTask.description, viewTask.color,
viewTask.due_date, viewTask.points,
viewTask.position, viewTask.column_id,
viewTask.comments, viewTask.attachments,
viewTask.assignees, viewTask.categories);
this.checkDueDate(); this.checkDueDate();
if (this.showActivity) { if (this.showActivity) {

View File

@ -19,7 +19,7 @@
</h4> </h4>
<div class="description" *ngIf="!isCollapsed" <div class="description" *ngIf="!isCollapsed"
[innerHtml]="getTaskDescription()"> [innerHtml]="taskData.html">
</div> </div>
<div class="stats"> <div class="stats">
@ -56,9 +56,71 @@
</div> </div>
<tb-context-menu> <tb-context-menu>
<tb-context-menu-item> <tb-context-menu-item (click)="viewTask()">
Task Test {{ strings['boards_viewTask'] }}
</tb-context-menu-item> </tb-context-menu-item>
<tb-context-menu-item (click)="editTask()">
{{ strings['boards_editTask'] }}
</tb-context-menu-item>
<tb-context-menu-item (click)="removeTask()">
{{ strings['boards_removeTask'] }}
</tb-context-menu-item>
<div *ngIf="boardsList && boardsList.length > 1">
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_copyTaskTo'] }}:
<i class="icon icon-help-circled"
attr.data-help="{{ strings['boards_copyMoveHelp'] }}"></i>
<select id="boardsList{{ taskData.id }}{{ strings['boards_copyTaskTo'].split(' ')[0] }}"
(change)="copyTaskToBoard($event)">
<option value="0">{{ strings['boards_selectBoard'] }}</option>
<ng-container *ngFor="let board of boardsList">
<option *ngIf="board.id !== activeBoard.id" [value]="board.id">
{{ board.name }}
</option>
</ng-container>
</select>
</tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_moveTaskTo'] }}:
<i class="icon icon-help-circled"
attr.data-help="{{ strings['boards_copyMoveHelp'] }}"></i>
<select id="boardsList{{ taskData.id }}{{
strings['boards_moveTaskTo'].split(' ')[0] }}"
(change)="moveTaskToBoard($event)">
<option value="0">{{ strings['boards_selectBoard'] }}</option>
<ng-container *ngFor="let board of boardsList">
<option *ngIf="board.id !== activeBoard.id" [value]="board.id">
{{ board.name }}
</option>
</ng-container>
</select>
</tb-context-menu-item>
</div>
<div *ngIf="activeBoard.columns.length > 1">
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_moveTask'] }}:
<select id="columnsList{{ taskData.id }}" (change)="changeTaskColumn($event)">
<option value="0">{{ strings['boards_selectColumn'] }}</option>
<option *ngFor="let col of activeBoard.columns" [value]="col.id">
{{ col.name }}
</option>
</select>
</tb-context-menu-item>
</div>
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item (click)="addTask()">
{{ strings['boards_addTask'] }}
</tb-context-menu-item>
</tb-context-menu> </tb-context-menu>
</div> </div>

View File

@ -7,9 +7,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import { import {
ApiResponse, ApiResponse,
Board, Board,
@ -31,8 +28,6 @@ import { BoardService } from '../board.service';
templateUrl: './task.component.html' templateUrl: './task.component.html'
}) })
export class TaskDisplay implements OnInit { export class TaskDisplay implements OnInit {
private totalTasks: number;
private completeTasks: number;
private isOverdue: boolean; private isOverdue: boolean;
private isNearlyDue: boolean; private isNearlyDue: boolean;
@ -54,7 +49,6 @@ export class TaskDisplay implements OnInit {
@Input('boards') @Input('boards')
set boards(boards: Array<Board>) { set boards(boards: Array<Board>) {
this.boardsList = boards; this.boardsList = boards;
// this.generateContextMenuItems();
} }
constructor(private auth: AuthService, constructor(private auth: AuthService,
@ -64,16 +58,10 @@ export class TaskDisplay implements OnInit {
private notes: NotificationsService, private notes: NotificationsService,
private stringsService: StringsService) { private stringsService: StringsService) {
this.onUpdateBoards = new EventEmitter<any>(); this.onUpdateBoards = new EventEmitter<any>();
this.totalTasks = 0;
this.completeTasks = 0;
this.percentComplete = 0; this.percentComplete = 0;
stringsService.stringsChanged.subscribe(newStrings => { stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings; this.strings = newStrings;
if (this.taskData) {
// this.generateContextMenuItems();
}
}); });
auth.userChanged.subscribe(() => { auth.userChanged.subscribe(() => {
@ -86,27 +74,21 @@ export class TaskDisplay implements OnInit {
} }
ngOnInit() { ngOnInit() {
// Since marked is global, the counts need to be stored uniquely per task.
// String literal access needed because augmenting the type doesn't work.
marked['taskCounts'] = []; // tslint:disable-line
if (!this.taskData) {
return;
}
marked['taskCounts'][this.taskData.id] = { // tslint:disable-line
total: 0,
complete: 0
};
// this.generateContextMenuItems();
this.initMarked();
this.calcPercentComplete();
this.checkDueDate(); this.checkDueDate();
this.convertTaskDescription();
} }
getTaskDescription(): string { private convertTaskDescription() {
let html = marked(this.taskData.description, this.markedCallback); let data = this.boardService.convertMarkdown(
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}'); this.taskData.description, this.markedCallback, true
);
data.html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
if (data.counts.total) {
this.percentComplete = data.counts.complete / data.counts.total;
}
this.taskData.html = this.sanitizer.bypassSecurityTrustHtml(data.html);
} }
getPercentStyle() { getPercentStyle() {
@ -131,7 +113,12 @@ export class TaskDisplay implements OnInit {
return yiq >= 140 ? '#333333' : '#efefef'; return yiq >= 140 ? '#333333' : '#efefef';
} }
changeTaskColumn() { changeTaskColumn(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('columnsList' + this.taskData.id) as HTMLSelectElement, let select = document.getElementById('columnsList' + this.taskData.id) as HTMLSelectElement,
id = +select[select.selectedIndex].value; id = +select[select.selectedIndex].value;
@ -151,7 +138,12 @@ export class TaskDisplay implements OnInit {
}); });
} }
copyTaskToBoard() { copyTaskToBoard(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('boardsList' + this.taskData.id + let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_copyTaskTo.split(' ')[0]) as HTMLSelectElement; this.strings.boards_copyTaskTo.split(' ')[0]) as HTMLSelectElement;
@ -184,7 +176,12 @@ export class TaskDisplay implements OnInit {
}); });
} }
moveTaskToBoard() { moveTaskToBoard(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('boardsList' + this.taskData.id + let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement; this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement;
@ -217,7 +214,7 @@ export class TaskDisplay implements OnInit {
} }
private checkDueDate() { private checkDueDate() {
if (this.taskData.due_date === '') { if (!this.taskData || this.taskData.due_date === '') {
return; return;
} }
@ -270,126 +267,5 @@ export class TaskDisplay implements OnInit {
return text; return text;
} }
// private getMoveMenuItem() {
// let menuText = this.strings.boards_moveTask +
// ': <select id="columnsList' + this.taskData.id + '" ' +
// '(click)="action($event)">' +
// '<option value="0">' + this.strings.boards_selectColumn + '</option>';
//
// this.activeBoard.columns.forEach((column: Column) => {
// menuText += '<option value="' + column.id + '">' + column.name + '</option>';
// });
//
// menuText += '</select>';
//
// let action = (event: any) => {
// if (event.target.tagName !== 'SELECT') {
// return;
// }
//
// this.changeTaskColumn();
// };
//
// return new ContextMenuItem(menuText, action, false, false, true);
// }
private calcPercentComplete() {
this.percentComplete = 0;
// String literal access needed because augmenting the type doesn't work.
marked['taskCounts'][this.taskData.id] = { // tslint:disable-line
total: 0,
complete: 0
};
marked(this.taskData.description);
if (marked['taskCounts'][this.taskData.id].total) { // tslint:disable-line
this.percentComplete = marked['taskCounts'][this.taskData.id].complete / // tslint:disable-line
marked['taskCounts'][this.taskData.id].total; // tslint:disable-line
}
}
// private generateContextMenuItems() {
// this.contextMenuItems = [
// new ContextMenuItem(this.strings.boards_viewTask, this.viewTask),
// new ContextMenuItem(this.strings.boards_editTask, this.editTask),
// new ContextMenuItem(this.strings.boards_removeTask, this.removeTask),
// new ContextMenuItem('', null, true),
// this.getMoveMenuItem(),
// new ContextMenuItem('', null, true),
// new ContextMenuItem(this.strings.boards_addTask, this.addTask)
// ];
//
// if (this.boardsList && this.boardsList.length > 1) {
// this.contextMenuItems
// .splice(3, 0,
// new ContextMenuItem('', null, true),
// this.getMenuItem(this.strings.boards_copyTaskTo),
// this.getMenuItem(this.strings.boards_moveTaskTo));
// }
// }
// private getMenuItem(text: string): ContextMenuItem {
// let menuText = text + ': ' +
// '<i class="icon icon-help-circled" ' +
// 'data-help="' + this.strings.boards_copyMoveHelp + '"></i> ' +
// '<select id="boardsList' + this.taskData.id + text.split(' ')[0] + '" ' +
// '(click)="action($event)">' +
// '<option value="0">' + this.strings.boards_selectBoard + '</option>';
//
// this.boardsList.forEach((board: Board) => {
// if (board.name !== this.activeBoard.name) {
// menuText += '<option value="' + board.id + '">' + board.name + '</option>';
// }
// });
//
// menuText += '</select>';
//
// let action = (event: any) => {
// if (event.target.tagName !== 'SELECT') {
// return;
// }
//
// if (text === this.strings.boards_copyTaskTo) {
// this.copyTaskToBoard();
// return;
// }
//
// this.moveTaskToBoard();
// };
//
// return new ContextMenuItem(menuText, action, false, false, true);
// }
private initMarked() {
let renderer = new marked.Renderer();
renderer.checkbox = isChecked => {
let text = '<i class="icon icon-check' + (isChecked ? '' : '-empty') + '"></i>';
return text;
};
renderer.link = (href, title, text) => {
let out = '<a href="' + href + '"';
if (title) {
out += ' title="' + title + '"';
}
out += ' target="tb_external" rel="noreferrer">' + text + '</a>';
return out;
};
marked.setOptions({
renderer,
smartypants: true,
highlight: code => {
return hljs.highlightAuto(code).value;
}
});
}
} }

View File

@ -1,2 +1,5 @@
<ng-content *ngIf="!isSeparator; else separator"></ng-content> <ng-content *ngIf="!isSeparator; else separator"
<ng-template #separator><hr></ng-template> class="context-menu-item"></ng-content>
<ng-template #separator class="context-menu-separator">
<hr>
</ng-template>

View File

@ -1,10 +1,4 @@
import { import { Component, ElementRef, EventEmitter, Input } from '@angular/core';
Component,
ElementRef,
EventEmitter,
Input,
Output
} from '@angular/core';
@Component({ @Component({
selector: 'tb-context-menu-item', selector: 'tb-context-menu-item',
@ -24,19 +18,17 @@ export class ContextMenuItem {
@Input() @Input()
isSeparator: boolean; isSeparator: boolean;
@Output() @Input()
clickEvent: EventEmitter<MouseEvent> = new EventEmitter(); isCustomEvent: boolean;
constructor(private el: ElementRef) { constructor(private el: ElementRef) {
const elem = el.nativeElement; const elem = el.nativeElement;
elem.onclick = (event) => { elem.onclick = (event) => {
if (this.isSeparator) { if (this.isSeparator || this.isCustomEvent) {
this.killEvent(event); this.killEvent(event);
return; return;
} }
this.clickEvent.next(event);
}; };
elem.oncontextmenu = (event) => { elem.oncontextmenu = (event) => {

View File

@ -1,4 +1,4 @@
<div class="context-menu-container" *ngIf="isOpen"> <div class="context-menu-container" style="top: -99999em;" *ngIf="isOpen">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@ -15,33 +15,43 @@ export class ContextMenu {
const parentEl = el.nativeElement.parentElement; const parentEl = el.nativeElement.parentElement;
if (!parentEl) {
return;
}
parentEl.oncontextmenu = (event: MouseEvent) => { parentEl.oncontextmenu = (event: MouseEvent) => {
this.parentEventHandler(event); this.parentEventHandler(event);
setTimeout(() => {
this.updatePosition(event);
}, 0);
}; };
} }
private updatePosition(event: MouseEvent) {
const edgeBuffer = 10;
// Adjust position if near an edge
const target = this.el.nativeElement.firstElementChild;
if (!target) {
return;
}
const rect = target.getBoundingClientRect();
const offsetX = (event.pageX + rect.width + edgeBuffer) > window.innerWidth;
const offsetY = (event.pageY + rect.height + edgeBuffer) > window.innerHeight;
target.style.left = event.pageX - (offsetX ? rect.width : 0) + 'px';
target.style.top = event.pageY - (offsetY ? rect.height : 0) + 'px';
}
private parentEventHandler(event: MouseEvent) { private parentEventHandler(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.menuService.closeAllMenus(); this.menuService.closeAllMenus();
this.el.nativeElement.style.top = '-10000px';
this.isOpen = true; this.isOpen = true;
const edgeBuffer = 10;
// Adjust position if near an edge
const adjustPosition = () => {
const target = this.el.nativeElement.firstElementChild;
const rect = target.getBoundingClientRect();
const offsetX = (event.pageX + rect.width + edgeBuffer) > window.innerWidth;
const offsetY = (event.pageY + rect.height + edgeBuffer) > window.innerHeight;
target.style.left = event.pageX - (offsetX ? rect.width : 0) + 'px';
target.style.top = event.pageY - (offsetY ? rect.height : 0) + 'px';
};
setTimeout(adjustPosition, 0);
} }
} }

View File

@ -1,3 +1,5 @@
import { SafeHtml } from '@angular/platform-browser';
import { Attachment } from './attachment.model'; import { Attachment } from './attachment.model';
import { Category } from './category.model'; import { Category } from './category.model';
import { Comment } from './comment.model'; import { Comment } from './comment.model';
@ -8,6 +10,7 @@ export class Task {
public attachments: Array<Attachment>; public attachments: Array<Attachment>;
public assignees: Array<User>; public assignees: Array<User>;
public categories: Array<Category>; public categories: Array<Category>;
public html: SafeHtml;
public filtered: boolean; public filtered: boolean;
public hideFiltered: boolean; public hideFiltered: boolean;

View File

@ -16,3 +16,20 @@
z-index: 100; z-index: 100;
} }
tb-context-menu-item {
cursor: pointer;
}
tb-context-menu-item:hover {
background-color: darken($white, 5%);
}
tb-context-menu-item[isseparator] {
cursor: default;
height: 1em;
}
tb-context-menu-item[isseparator] {
background-color: $white;
}

View File

@ -63,7 +63,7 @@ describe('ApiInterceptor', () => {
it('adds Authorization header when JWT is present', it('adds Authorization header when JWT is present',
inject([HttpClient, HttpTestingController], inject([HttpClient, HttpTestingController],
(http: HttpClient, httpMock: HttpTestingController) => { (http: HttpClient, httpMock: HttpTestingController) => {
localStorage.setItem('taskboard.jwt', 'fake'); sessionStorage.setItem('taskboard.jwt', 'fake');
http.post('', {}).subscribe(response => { http.post('', {}).subscribe(response => {
expect(response).toBeTruthy(); expect(response).toBeTruthy();
@ -76,7 +76,7 @@ describe('ApiInterceptor', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
req.flush({ data: ['newToken'] }); req.flush({ data: ['newToken'] });
expect(localStorage.getItem('taskboard.jwt')).toEqual('newToken'); expect(sessionStorage.getItem('taskboard.jwt')).toEqual('newToken');
} }
) )
) )
@ -84,7 +84,7 @@ describe('ApiInterceptor', () => {
it('handles errors and clears the JWT', it('handles errors and clears the JWT',
inject([HttpClient, HttpTestingController], inject([HttpClient, HttpTestingController],
(http: HttpClient, httpMock: HttpTestingController) => { (http: HttpClient, httpMock: HttpTestingController) => {
localStorage.setItem('taskboard.jwt', 'fake'); sessionStorage.setItem('taskboard.jwt', 'fake');
http.get('').subscribe(response => { http.get('').subscribe(response => {
expect(response).toBeTruthy(); expect(response).toBeTruthy();
@ -100,7 +100,7 @@ describe('ApiInterceptor', () => {
const error = new HttpErrorResponse({ status: 401 }); const error = new HttpErrorResponse({ status: 401 });
req.flush(error); req.flush(error);
expect(localStorage.getItem('Authorization')).toEqual(null); expect(sessionStorage.getItem('Authorization')).toEqual(null);
} }
) )
) )

View File

@ -20,8 +20,6 @@ describe('ContextMenu', () => {
fixture: ComponentFixture<ContextMenu>, fixture: ComponentFixture<ContextMenu>,
elementRef: ElementRefMock; elementRef: ElementRefMock;
const getPrivateFunction = name => component[name].bind(component);
beforeEach(() => { beforeEach(() => {
elementRef = new ElementRefMock(); elementRef = new ElementRefMock();