WIP - New context menu impl
This commit is contained in:
parent
c58e54b176
commit
044b72c820
@ -20,5 +20,5 @@ after_success:
|
||||
- touch 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/app coverage-$(php version.php)/
|
||||
- ./dropbox_uploader.sh -f fakeconfig upload coverage/app/lcov-report coverage-$(php version.php)/
|
||||
|
||||
|
@ -155,11 +155,11 @@ Because I like seeing the numbers.
|
||||
|
||||
Language | Files | Blank | Comment | Code
|
||||
-------------|--------:|---------:|--------:|---------:
|
||||
TypeScript | 64 | 925 | 86 | 4091
|
||||
TypeScript | 63 | 904 | 78 | 3932
|
||||
PHP | 19 | 624 | 27 | 1997
|
||||
HTML | 19 | 152 | 1 | 1422
|
||||
SASS | 14 | 269 | 12 | 1215
|
||||
__SUM:__ | __116__ | __1970__ | __126__ | __8725__
|
||||
HTML | 20 | 159 | 0 | 1479
|
||||
SASS | 14 | 269 | 12 | 1211
|
||||
__SUM:__ | __116__ | __1956__ | __117__ | __8619__
|
||||
|
||||
Command: `cloc --exclude-dir=vendor --exclude-ext=json src/`
|
||||
|
||||
|
1269
package-lock.json
generated
1269
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@ -40,15 +40,15 @@
|
||||
"postinstall": "cd src/api/ && composer update && composer install --optimize-autoloader && cd ../../"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^6.0.4",
|
||||
"@angular/common": "^6.0.4",
|
||||
"@angular/compiler": "^6.0.4",
|
||||
"@angular/core": "^6.0.4",
|
||||
"@angular/forms": "^6.0.4",
|
||||
"@angular/http": "^6.0.4",
|
||||
"@angular/platform-browser": "^6.0.4",
|
||||
"@angular/platform-browser-dynamic": "^6.0.4",
|
||||
"@angular/router": "^6.0.4",
|
||||
"@angular/animations": "^6.0.7",
|
||||
"@angular/common": "^6.0.7",
|
||||
"@angular/compiler": "^6.0.7",
|
||||
"@angular/core": "^6.0.7",
|
||||
"@angular/forms": "^6.0.7",
|
||||
"@angular/http": "^6.0.7",
|
||||
"@angular/platform-browser": "^6.0.7",
|
||||
"@angular/platform-browser-dynamic": "^6.0.7",
|
||||
"@angular/router": "^6.0.7",
|
||||
"chartist": "^0.11.0",
|
||||
"chartist-plugin-tooltips": "^0.0.17",
|
||||
"classlist.js": "^1.1.20150312",
|
||||
@ -58,25 +58,25 @@
|
||||
"marked": "^0.4.0",
|
||||
"ng2-dragula": "^1.5.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"rxjs": "^6.2.0",
|
||||
"rxjs": "^6.2.1",
|
||||
"scss-base": "^1.4.0",
|
||||
"zone.js": "^0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.6.5",
|
||||
"@angular-devkit/build-angular": "~0.6.8",
|
||||
"@angular/cli": "^6.0.8",
|
||||
"@angular/compiler-cli": "^6.0.4",
|
||||
"@angular/language-service": "^6.0.4",
|
||||
"@types/jasmine": "~2.8.7",
|
||||
"@angular/compiler-cli": "^6.0.7",
|
||||
"@angular/language-service": "^6.0.7",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~10.1.4",
|
||||
"bourbon": "5.0.0",
|
||||
"@types/node": "~10.5.0",
|
||||
"bourbon": "5.0.1",
|
||||
"bourbon-neat": "1.9.0",
|
||||
"codelyzer": "^4.3.0",
|
||||
"codelyzer": "^4.4.2",
|
||||
"jasmine": "^3.1.0",
|
||||
"jasmine-core": "^3.1.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~2.0.2",
|
||||
"karma": "~2.0.4",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "^2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
@ -85,7 +85,7 @@
|
||||
"nodemon": "^1.17.5",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"protractor": "~5.3.2",
|
||||
"ts-node": "~6.0.5",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.10.0",
|
||||
"typescript": "^2.7.2"
|
||||
}
|
||||
|
10
src/api/composer.lock
generated
10
src/api/composer.lock
generated
@ -258,16 +258,16 @@
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6"
|
||||
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/478465659fd987669df0bd8a9bf22a8710e5f1b6",
|
||||
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
||||
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -302,7 +302,7 @@
|
||||
"object",
|
||||
"object graph"
|
||||
],
|
||||
"time": "2018-05-29T17:25:09+00:00"
|
||||
"time": "2018-06-11T23:09:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
|
@ -26,7 +26,7 @@ export class ApiInterceptor implements HttpInterceptor {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const token = localStorage.getItem(this.JWT_KEY);
|
||||
const token = sessionStorage.getItem(this.JWT_KEY);
|
||||
|
||||
if (token !== null) {
|
||||
headers['Authorization'] = token;
|
||||
@ -45,14 +45,14 @@ export class ApiInterceptor implements HttpInterceptor {
|
||||
|
||||
if ((evt.status === 401 || evt.status === 400) &&
|
||||
(evt.url + '').indexOf('login') === -1) {
|
||||
localStorage.removeItem(this.JWT_KEY);
|
||||
sessionStorage.removeItem(this.JWT_KEY);
|
||||
this.router.navigate(['']);
|
||||
return;
|
||||
}
|
||||
|
||||
const response: ApiResponse = evt.body;
|
||||
if (response.data) {
|
||||
localStorage.setItem(this.JWT_KEY, response.data[0]);
|
||||
sessionStorage.setItem(this.JWT_KEY, response.data[0]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -76,12 +76,7 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
|
||||
|
||||
this.pageName = this.strings.boards;
|
||||
|
||||
sub = this.boardService.getBoards().subscribe((response: ApiResponse) => {
|
||||
this.boards = [];
|
||||
this.updateBoardsList(response.data[1]);
|
||||
this.loading = false;
|
||||
});
|
||||
this.subs.push(sub);
|
||||
this.updateBoards();
|
||||
|
||||
sub = boardService.activeBoardChanged.subscribe((board: 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,
|
||||
toColumnId = +value[2].parentNode.id,
|
||||
fromColumnId = +value[3].parentNode.id;
|
||||
@ -146,6 +148,8 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
|
||||
fromColumnId = -1;
|
||||
}
|
||||
|
||||
let updateList = [];
|
||||
|
||||
this.activeBoard.columns.forEach(column => {
|
||||
if (column.id === toColumnId || column.id === fromColumnId) {
|
||||
let position = 1,
|
||||
@ -158,10 +162,17 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
|
||||
position++;
|
||||
});
|
||||
|
||||
let oneOff = this.boardService.updateColumn(column).subscribe();
|
||||
oneOff.unsubscribe();
|
||||
updateList.push(column);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private updateBoards(): void {
|
||||
this.boardService.getBoards().subscribe((response: ApiResponse) => {
|
||||
this.boards = [];
|
||||
this.updateBoardsList(response.data[1]);
|
||||
});
|
||||
}
|
||||
|
||||
private updateBoardsList(boards: Array<any>): void {
|
||||
let activeBoards: Array<Board> = [];
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import * as marked from 'marked';
|
||||
import * as hljs from 'highlight.js';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
|
||||
@ -13,13 +15,42 @@ import {
|
||||
User
|
||||
} from '../shared/models';
|
||||
|
||||
interface MarkedReturn {
|
||||
html: string;
|
||||
counts: any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BoardService {
|
||||
private checkCounts = {
|
||||
total: 0,
|
||||
complete: 0
|
||||
}
|
||||
private activeBoard = new BehaviorSubject<Board>(null);
|
||||
private defaultCallback = (err: any, text: string) => {
|
||||
console.log('default', err, text);
|
||||
return text;
|
||||
};
|
||||
|
||||
public activeBoardChanged = this.activeBoard.asObservable();
|
||||
|
||||
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 {
|
||||
@ -124,5 +155,42 @@ export class BoardService {
|
||||
boardData.ownIssuetracker,
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,9 @@
|
||||
</div>
|
||||
|
||||
<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-modal *ngIf="activeBoard && columnData"
|
||||
@ -108,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
<div class="description" *ngIf="viewModalProps.description.length"
|
||||
[innerHtml]="getTaskDescription()">
|
||||
[innerHtml]="viewModalProps.html">
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
|
@ -9,9 +9,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
import * as marked from 'marked';
|
||||
import * as hljs from 'highlight.js';
|
||||
|
||||
import {
|
||||
ApiResponse,
|
||||
ActivitySimple,
|
||||
@ -74,7 +71,8 @@ export class ColumnDisplay implements OnInit, OnDestroy {
|
||||
@Input('column') columnData: Column;
|
||||
@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,
|
||||
private auth: AuthService,
|
||||
@ -99,11 +97,6 @@ export class ColumnDisplay implements OnInit, OnDestroy {
|
||||
|
||||
let sub = stringsService.stringsChanged.subscribe(newStrings => {
|
||||
this.strings = newStrings;
|
||||
|
||||
// this.contextMenuItems = [
|
||||
// new ContextMenuItem(this.strings.boards_addTask,
|
||||
// this.getShowModalFunction())
|
||||
// ];
|
||||
});
|
||||
this.subs.push(sub);
|
||||
|
||||
@ -477,6 +470,11 @@ export class ColumnDisplay implements OnInit, OnDestroy {
|
||||
updatedTask.ownAttachment,
|
||||
updatedTask.sharedUser,
|
||||
updatedTask.sharedCategory);
|
||||
const data = this.boardService.convertMarkdown(task.description,
|
||||
(_, text) => { return text; }, true);
|
||||
|
||||
task.html = data.html;
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
@ -536,14 +534,9 @@ export class ColumnDisplay implements OnInit, OnDestroy {
|
||||
return text;
|
||||
}
|
||||
|
||||
private getTaskDescription() {
|
||||
let html = marked(this.viewModalProps.description, this.markedCallback);
|
||||
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
|
||||
}
|
||||
|
||||
private getComment(text: string) {
|
||||
let html = marked(text, this.markedCallback);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||
let data = this.boardService.convertMarkdown(text, this.markedCallback);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(data.html);
|
||||
}
|
||||
|
||||
private getUserName(userId: number) {
|
||||
@ -579,12 +572,7 @@ export class ColumnDisplay implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.newComment = '';
|
||||
this.viewModalProps = new Task(viewTask.id, viewTask.title,
|
||||
viewTask.description, viewTask.color,
|
||||
viewTask.due_date, viewTask.points,
|
||||
viewTask.position, viewTask.column_id,
|
||||
viewTask.comments, viewTask.attachments,
|
||||
viewTask.assignees, viewTask.categories);
|
||||
this.viewModalProps = this.convertToTask(viewTask);
|
||||
this.checkDueDate();
|
||||
|
||||
if (this.showActivity) {
|
||||
|
@ -19,7 +19,7 @@
|
||||
</h4>
|
||||
|
||||
<div class="description" *ngIf="!isCollapsed"
|
||||
[innerHtml]="getTaskDescription()">
|
||||
[innerHtml]="taskData.html">
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
@ -56,9 +56,71 @@
|
||||
</div>
|
||||
|
||||
<tb-context-menu>
|
||||
<tb-context-menu-item>
|
||||
Task Test
|
||||
<tb-context-menu-item (click)="viewTask()">
|
||||
{{ strings['boards_viewTask'] }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
@ -7,9 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
import * as marked from 'marked';
|
||||
import * as hljs from 'highlight.js';
|
||||
|
||||
import {
|
||||
ApiResponse,
|
||||
Board,
|
||||
@ -31,8 +28,6 @@ import { BoardService } from '../board.service';
|
||||
templateUrl: './task.component.html'
|
||||
})
|
||||
export class TaskDisplay implements OnInit {
|
||||
private totalTasks: number;
|
||||
private completeTasks: number;
|
||||
private isOverdue: boolean;
|
||||
private isNearlyDue: boolean;
|
||||
|
||||
@ -54,7 +49,6 @@ export class TaskDisplay implements OnInit {
|
||||
@Input('boards')
|
||||
set boards(boards: Array<Board>) {
|
||||
this.boardsList = boards;
|
||||
// this.generateContextMenuItems();
|
||||
}
|
||||
|
||||
constructor(private auth: AuthService,
|
||||
@ -64,16 +58,10 @@ export class TaskDisplay implements OnInit {
|
||||
private notes: NotificationsService,
|
||||
private stringsService: StringsService) {
|
||||
this.onUpdateBoards = new EventEmitter<any>();
|
||||
this.totalTasks = 0;
|
||||
this.completeTasks = 0;
|
||||
this.percentComplete = 0;
|
||||
|
||||
stringsService.stringsChanged.subscribe(newStrings => {
|
||||
this.strings = newStrings;
|
||||
|
||||
if (this.taskData) {
|
||||
// this.generateContextMenuItems();
|
||||
}
|
||||
});
|
||||
|
||||
auth.userChanged.subscribe(() => {
|
||||
@ -86,27 +74,21 @@ export class TaskDisplay implements OnInit {
|
||||
}
|
||||
|
||||
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.convertTaskDescription();
|
||||
}
|
||||
|
||||
getTaskDescription(): string {
|
||||
let html = marked(this.taskData.description, this.markedCallback);
|
||||
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
|
||||
private convertTaskDescription() {
|
||||
let data = this.boardService.convertMarkdown(
|
||||
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() {
|
||||
@ -131,7 +113,12 @@ export class TaskDisplay implements OnInit {
|
||||
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,
|
||||
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 +
|
||||
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 +
|
||||
this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement;
|
||||
|
||||
@ -217,7 +214,7 @@ export class TaskDisplay implements OnInit {
|
||||
}
|
||||
|
||||
private checkDueDate() {
|
||||
if (this.taskData.due_date === '') {
|
||||
if (!this.taskData || this.taskData.due_date === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -270,126 +267,5 @@ export class TaskDisplay implements OnInit {
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,5 @@
|
||||
<ng-content *ngIf="!isSeparator; else separator"></ng-content>
|
||||
<ng-template #separator><hr></ng-template>
|
||||
<ng-content *ngIf="!isSeparator; else separator"
|
||||
class="context-menu-item"></ng-content>
|
||||
<ng-template #separator class="context-menu-separator">
|
||||
<hr>
|
||||
</ng-template>
|
||||
|
@ -1,10 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { Component, ElementRef, EventEmitter, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-context-menu-item',
|
||||
@ -24,19 +18,17 @@ export class ContextMenuItem {
|
||||
@Input()
|
||||
isSeparator: boolean;
|
||||
|
||||
@Output()
|
||||
clickEvent: EventEmitter<MouseEvent> = new EventEmitter();
|
||||
@Input()
|
||||
isCustomEvent: boolean;
|
||||
|
||||
constructor(private el: ElementRef) {
|
||||
const elem = el.nativeElement;
|
||||
|
||||
elem.onclick = (event) => {
|
||||
if (this.isSeparator) {
|
||||
if (this.isSeparator || this.isCustomEvent) {
|
||||
this.killEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clickEvent.next(event);
|
||||
};
|
||||
|
||||
elem.oncontextmenu = (event) => {
|
||||
|
@ -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>
|
||||
</div>
|
||||
|
||||
|
@ -15,33 +15,43 @@ export class ContextMenu {
|
||||
|
||||
const parentEl = el.nativeElement.parentElement;
|
||||
|
||||
if (!parentEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
parentEl.oncontextmenu = (event: MouseEvent) => {
|
||||
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) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.menuService.closeAllMenus();
|
||||
this.el.nativeElement.style.top = '-10000px';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
import { Attachment } from './attachment.model';
|
||||
import { Category } from './category.model';
|
||||
import { Comment } from './comment.model';
|
||||
@ -8,6 +10,7 @@ export class Task {
|
||||
public attachments: Array<Attachment>;
|
||||
public assignees: Array<User>;
|
||||
public categories: Array<Category>;
|
||||
public html: SafeHtml;
|
||||
|
||||
public filtered: boolean;
|
||||
public hideFiltered: boolean;
|
||||
|
@ -16,3 +16,20 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ describe('ApiInterceptor', () => {
|
||||
it('adds Authorization header when JWT is present',
|
||||
inject([HttpClient, HttpTestingController],
|
||||
(http: HttpClient, httpMock: HttpTestingController) => {
|
||||
localStorage.setItem('taskboard.jwt', 'fake');
|
||||
sessionStorage.setItem('taskboard.jwt', 'fake');
|
||||
|
||||
http.post('', {}).subscribe(response => {
|
||||
expect(response).toBeTruthy();
|
||||
@ -76,7 +76,7 @@ describe('ApiInterceptor', () => {
|
||||
expect(req.request.method).toEqual('POST');
|
||||
|
||||
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',
|
||||
inject([HttpClient, HttpTestingController],
|
||||
(http: HttpClient, httpMock: HttpTestingController) => {
|
||||
localStorage.setItem('taskboard.jwt', 'fake');
|
||||
sessionStorage.setItem('taskboard.jwt', 'fake');
|
||||
|
||||
http.get('').subscribe(response => {
|
||||
expect(response).toBeTruthy();
|
||||
@ -100,7 +100,7 @@ describe('ApiInterceptor', () => {
|
||||
|
||||
const error = new HttpErrorResponse({ status: 401 });
|
||||
req.flush(error);
|
||||
expect(localStorage.getItem('Authorization')).toEqual(null);
|
||||
expect(sessionStorage.getItem('Authorization')).toEqual(null);
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -20,8 +20,6 @@ describe('ContextMenu', () => {
|
||||
fixture: ComponentFixture<ContextMenu>,
|
||||
elementRef: ElementRefMock;
|
||||
|
||||
const getPrivateFunction = name => component[name].bind(component);
|
||||
|
||||
beforeEach(() => {
|
||||
elementRef = new ElementRefMock();
|
||||
|
||||
|
Reference in New Issue
Block a user