This commit is contained in:
kiswa 2017-08-03 16:03:54 +00:00
parent f5fc653fff
commit 0510e1d835
16 changed files with 292 additions and 42 deletions

10
src/api/composer.lock generated
View File

@ -1643,16 +1643,16 @@
},
{
"name": "symfony/yaml",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "1f93a8d19b8241617f5074a123e282575b821df8"
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/1f93a8d19b8241617f5074a123e282575b821df8",
"reference": "1f93a8d19b8241617f5074a123e282575b821df8",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"shasum": ""
},
"require": {
@ -1694,7 +1694,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-06-15T12:58:50+00:00"
"time": "2017-07-23T12:43:26+00:00"
},
{
"name": "webmozart/assert",

View File

@ -59,6 +59,8 @@ class Tasks extends BaseController {
R::store($task);
$actor = R::load('user', Auth::GetUserId($request));
$this->updateTaskOrder($task, $actor);
$this->dbLogger->logChange($actor->id,
$actor->username . ' added task ' . $task->title . '.',
'', json_encode($task), 'task', $task->id);
@ -168,5 +170,26 @@ class Tasks extends BaseController {
return $column->board_id;
}
private function updateTaskOrder($task, $user) {
$column = R::load('column', $task->column_id);
$user_opts = R::load('useroption', $user->user_option_id);
$index = count($column->xownTaskList);
$newTask = $column->xownTaskList[$index];
if ($user_opts->new_tasks_at_bottom) {
$newTask->position = $index;
R::store($newTask);
return;
}
for ($i = count($column->xownTaskList); $i > 0; --$i) {
$updateTask = $column->xownTaskList[$i];
$updateTask->position = $i;
R::store($column);
}
}
}

View File

@ -64,6 +64,7 @@
<tb-column class="column"
*ngFor="let column of activeBoard.columns"
[column]="column"
[boards]="boards"></tb-column>
[boards]="boards"
(on-update-boards)="updateBoards()"></tb-column>
</div>

View File

@ -50,7 +50,6 @@ export class BoardDisplay implements OnInit {
this.userFilter = null;
this.categoryFilter = null;
this.pageName = 'Boards';
this.loading = true;
stringsService.stringsChanged.subscribe(newStrings => {
@ -62,10 +61,8 @@ export class BoardDisplay implements OnInit {
}
});
boardService.getBoards().subscribe((response: ApiResponse) => {
this.updateBoardsList(response.data[1]);
this.loading = false;
});
this.pageName = this.strings.boards;
this.updateBoards();
boardService.activeBoardChanged.subscribe((board: Board) => {
if (!board) {
@ -107,6 +104,14 @@ export class BoardDisplay implements OnInit {
this.router.navigate(['/boards/' + this.boardNavId]);
}
updateBoards() {
this.boardService.getBoards().subscribe((response: ApiResponse) => {
this.boards = [];
this.updateBoardsList(response.data[1]);
this.loading = false;
});
}
private updateBoardsList(boards: Array<any>): void {
let activeBoards: Array<Board> = [];

View File

@ -27,7 +27,7 @@
[title]="strings['boards_collapseColumn']" (click)="toggleCollapsed()"></span>
<span class="count-editor"
*ngIf="activeUser.isAdmin() || activeUser.isBoardAdmin()">
*ngIf="activeUser.isAnyAdmin()">
<i class="icon icon-hashtag"
[title]="strings['boards_editTaskLimit']"
(click)="beginLimitEdit()"></i>
@ -44,6 +44,21 @@
(click)="saveLimitChanges()"></i>
</div>
</span>
<span class="sort-by">
{{ strings['sortBy'] }}:
<select [(ngModel)]="sortOption"
(change)="sortTasks()">
<option value="pos">
{{ strings['boards_sortByPosition'] }}
</option>
<option value="due">
{{ strings['boards_sortByDueDate'] }}
</option>
<option value="pnt">
{{ strings['boards_sortByPoints'] }}
</option>
</select>
</span>
</h3>
<div class="quick-add">
@ -62,6 +77,7 @@
[add-task]="getShowModalFunction()"
[edit-task]="getShowModalFunction(task.id)"
[remove-task]="getRemoveTaskFunction(task.id)"
(on-update-boards)="callBoardUpdate();"
[collapse]="collapseTasks"></tb-task>
</div>
@ -75,7 +91,7 @@
</div>
<div class="buttons">
<button class="flat"
(click)="removeTask()">{{ strings['yes'] }}</button>
(click)="modal.close(MODAL_CONFIRM_ID + columnData.id);removeTask()">{{ strings['yes'] }}</button>
<button #defaultAction #focusMe
(click)="modal.close(MODAL_CONFIRM_ID + columnData.id)">
{{ strings['no'] }}

View File

@ -1,8 +1,10 @@
import {
Component,
Input,
ElementRef,
OnInit
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import {
@ -41,6 +43,7 @@ export class ColumnDisplay implements OnInit {
private tasks: Array<Task>;
private contextMenuItems: Array<ContextMenuItem>;
private sortOption: string;
private MODAL_ID: string;
private MODAL_CONFIRM_ID: string;
@ -53,6 +56,8 @@ export class ColumnDisplay implements OnInit {
@Input('column') columnData: Column;
@Input('boards') boards: Array<Board>;
@Output('on-update-boards') onUpdateBoards: EventEmitter<any> = new EventEmitter<any>();
constructor(private elRef: ElementRef,
private auth: AuthService,
private notes: NotificationsService,
@ -62,6 +67,7 @@ export class ColumnDisplay implements OnInit {
this.templateElement = elRef.nativeElement;
this.tasks = [];
this.collapseTasks = false;
this.sortOption = 'pos';
this.MODAL_ID = 'add-task-form-';
this.MODAL_CONFIRM_ID = 'task-remove-confirm';
@ -118,6 +124,27 @@ export class ColumnDisplay implements OnInit {
this.taskLimit = this.columnData.task_limit;
}
sortTasks() {
switch (this.sortOption) {
case 'pos':
this.columnData.tasks.sort((a, b) => {
return b.position - a.position;
});
break;
case 'due':
this.columnData.tasks.sort((a, b) => {
return new Date(a.due_date).getTime() -
new Date(b.due_date).getTime();
});
break;
case 'pnt':
this.columnData.tasks.sort((a, b) => {
return b.points - a.points;
});
break;
}
}
toggleCollapsed() {
this.templateElement.classList.toggle('collapsed');
@ -163,6 +190,7 @@ export class ColumnDisplay implements OnInit {
});
this.boardService.updateActiveBoard(boardData);
this.boardService.refreshToken();
});
}
@ -192,6 +220,7 @@ export class ColumnDisplay implements OnInit {
}
this.boardService.updateActiveBoard(response.data[1][0]);
this.boardService.refreshToken();
});
}
@ -240,6 +269,10 @@ export class ColumnDisplay implements OnInit {
return true;
}
private callBoardUpdate() {
this.onUpdateBoards.emit();
}
private getRemoveTaskFunction(taskId: number): Function {
return () => {
this.taskToRemove = taskId;

View File

@ -18,6 +18,7 @@
<div class="description" *ngIf="!isCollapsed"
[innerHTML]="getTaskDescription()"></div>
<pre>{{ taskData.position | json }}</pre>
<div class="stats">
<span *ngIf="userOptions.show_assignee">

View File

@ -1,7 +1,9 @@
import {
Component,
EventEmitter,
Input,
OnInit
OnInit,
Output
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -47,6 +49,8 @@ export class TaskDisplay implements OnInit {
@Input('remove-task') removeTask: Function;
@Input('collapse') isCollapsed: boolean;
@Output('on-update-boards') onUpdateBoards: EventEmitter<any>;
@Input('boards')
set boards(boards: Array<Board>) {
this.boardsList = boards;
@ -59,6 +63,7 @@ export class TaskDisplay implements OnInit {
private modal: ModalService,
private notes: NotificationsService,
private stringsService: StringsService) {
this.onUpdateBoards = new EventEmitter<any>();
this.totalTasks = 0;
this.completeTasks = 0;
this.percentComplete = 0;
@ -97,7 +102,7 @@ export class TaskDisplay implements OnInit {
getTaskDescription(): SafeHtml {
let html = this.sanitizer.bypassSecurityTrustHtml(
marked(this.taskData.description));
marked(this.taskData.description, this.markedCallback));
return html;
}
@ -124,6 +129,36 @@ export class TaskDisplay implements OnInit {
return yiq >= 140 ? '#333333' : '#efefef';
}
// Needs anonymous function for proper `this` context.
private markedCallback = (error: any, text: string) => {
this.activeBoard.issue_trackers.forEach(tracker => {
let re = new RegExp(tracker.regex, 'ig');
let replacements = new Array<any>();
let result = re.exec(text);
while (result !== null) {
let link = '<a href="' +
tracker.url.replace(/%BUGID%/g, result[1]) +
'" target="tb_external" rel="noreferrer">' +
result[0] + '</a>';
// text = text.replace(result[0], link);
replacements.push({
str: result[0],
link
});
result = re.exec(text);
}
for (let i = replacements.length - 1; i >= 0; --i) {
text = text.replace(replacements[i].str,
replacements[i].link);
}
});
return text;
}
private getMoveMenuItem() {
let menuText = this.strings.boards_moveTask +
': <select id="columnsList' + this.taskData.id + '">';
@ -148,11 +183,9 @@ export class TaskDisplay implements OnInit {
.subscribe((response: ApiResponse) => {
response.alerts.forEach(note => this.notes.add(note));
if (response.status !== 'success') {
return;
if (response.status === 'success') {
this.boardService.updateActiveBoard(response.data[2][0]);
}
this.boardService.updateActiveBoard(response.data[2][0]);
});
}
@ -184,7 +217,7 @@ export class TaskDisplay implements OnInit {
new ContextMenuItem(this.strings.boards_addTask, this.addTask)
];
if (this.boardsList.length > 1) {
if (this.boardsList && this.boardsList.length > 1) {
this.contextMenuItems
.splice(3, 0,
new ContextMenuItem('', null, true),
@ -197,7 +230,7 @@ export class TaskDisplay implements OnInit {
let menuText = text + ': ' +
'<i class="icon icon-help-circled" ' +
'data-help="' + this.strings.boards_copyMoveHelp + '"></i> ' +
'<select id="boardsList' + text + '">';
'<select id="boardsList' + text.split(' ')[0] + '">';
this.boardsList.forEach((board: Board) => {
if (board.name !== this.activeBoard.name) {
@ -207,7 +240,81 @@ export class TaskDisplay implements OnInit {
menuText += '</select>';
return new ContextMenuItem(menuText, null, false, false);
let action = () => {
if (text === this.strings.boards_copyTaskTo) {
this.copyTaskToBoard();
return;
}
this.moveTaskToBoard();
};
return new ContextMenuItem(menuText, action, false, false);
}
private copyTaskToBoard() {
let select = document.getElementById('boardsList' +
this.strings.boards_copyTaskTo.split(' ')[0]) as HTMLSelectElement;
let newBoardId = +select[select.selectedIndex].value;
let taskData = { ...this.taskData };
let boardData: Board;
this.boardsList.forEach(board => {
if (board.id === newBoardId) {
taskData.column_id = board.columns[0].id;
boardData = board;
}
});
this.boardService.addTask(taskData)
.subscribe((response: ApiResponse) => {
if (response.status === 'success') {
this.notes.add(
new Notification('success',
this.strings.boards_task +
' ' + taskData.title + ' ' +
this.strings.boards_taskCopied +
' ' + boardData.name));
this.onUpdateBoards.emit();
return;
}
response.alerts.forEach(note => this.notes.add(note));
});
}
private moveTaskToBoard() {
let select = document.getElementById('boardsList' +
this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement;
let newBoardId = +select[select.selectedIndex].value;
let boardData: Board;
this.boardsList.forEach(board => {
if (board.id === newBoardId) {
this.taskData.column_id = board.columns[0].id;
boardData = board;
}
});
this.boardService.updateTask(this.taskData)
.subscribe((response: ApiResponse) => {
if (response.status === 'success') {
this.notes.add(
new Notification('success',
this.strings.boards_task +
' ' + this.taskData.title + ' ' +
this.strings.boards_taskMoved +
' ' + boardData.name));
this.onUpdateBoards.emit();
return;
}
response.alerts.forEach(note => this.notes.add(note));
});
}
private initMarked() {
@ -240,7 +347,7 @@ export class TaskDisplay implements OnInit {
out += ' title="' + title + '"';
}
out += ' target="tb_external">' + text + '</a>';
out += ' target="tb_external" rel="noreferrer">' + text + '</a>';
return out;
};

View File

@ -28,7 +28,7 @@
</label>
<label class="inline right">
{{ strings['settings_sortBy'] }}:
{{ strings['sortBy'] }}:
<select class="autosize" [(ngModel)]="sortFilter"
(change)="filterBoards()">
<option value="name-asc">{{ strings['settings_name'] }} (A-Z)</option>

View File

@ -3,11 +3,10 @@
<div class="menu-item" *ngFor="let item of menuItems"
[ngClass]="{ 'no-highlight': item.isSeparator || !item.canHighlight }"
(click)="callAction(item.action)">
(click)="callAction($event, item.action)">
<hr *ngIf="item.isSeparator">
<div *ngIf="!item.isSeparator"[innerHTML]="getText(item)"></div>
</div>
</div>

View File

@ -33,10 +33,15 @@ export class ContextMenu {
return this.sanitizer.bypassSecurityTrustHtml(item.text);
}
callAction(action: Function) {
callAction(event: MouseEvent, action: Function) {
event.preventDefault();
event.stopPropagation();
if (action) {
action();
}
this.menuService.closeAllMenus();
}
private eventHandler(event: MouseEvent) {

View File

@ -9,7 +9,7 @@ export class ContextMenuService {
constructor() {
this.menus = [];
document.addEventListener('click', () => {
document.addEventListener('click', event => {
this.closeAllMenus();
});
}

View File

@ -5,6 +5,7 @@
"no": "No",
"save": "Save",
"cancel": "Cancel",
"sortBy": "Sort By",
"dashboard": "Dashboard",
"boards": "Boards",
@ -65,7 +66,6 @@
"settings_active": "Active",
"settings_inactive": "Inactive",
"settings_anyUser": "Any User",
"settings_sortBy": "Sort By",
"settings_creationNew": "Creation (New First)",
"settings_creationOld": "Creation (Old First)",
"settings_name": "Name",
@ -86,7 +86,7 @@
"settings_defaultTaskColor": "Default Task Color",
"settings_addCategory": "Add Category",
"settings_issueTrackers": "Issue Trackers",
"settings_issueTrackersHelp": "Example URL: https://github.com/kiswa/TaskBoard/issues/%BUGID\\1% Example RegExp: (?:Issue)?#(\\d+)",
"settings_issueTrackersHelp": "Example URL: https://github.com/kiswa/TaskBoard/issues/%BUGID% Example RegExp: (?:Issue)?#(\\d+)",
"settings_issueTrakcerUrl": "Issue Tracker URL - use %BUGID% as placeholder",
"settings_issueTrackerRegExp": "BUGID RegExp",
"settings_addIssueTracker": "Add Issue Tracker",
@ -186,6 +186,13 @@
"boards_taskCategory": "Category",
"boards_task": "Task",
"boards_taskComplete": "Complete"
"boards_taskComplete": "Complete",
"boards_taskCopied": "copied to board",
"boards_taskMoved": "moved to board",
"boards_sortByPosition": "Position",
"boards_sortByDueDate": "Due Date",
"boards_sortByLastModified": "Last Modifed",
"boards_sortByPoints": "Points"
}

View File

@ -5,6 +5,7 @@
"no": "No",
"save": "Guardar",
"cancel": "Cancelar",
"sortBy": "Ordenar por",
"dashboard": "Salpicadero",
"boards": "Tableros",
@ -65,7 +66,6 @@
"settings_active": "Activo",
"settings_inactive": "Inactiva",
"settings_anyUser": "Cualquier Usuario",
"settings_sortBy": "Ordenar por",
"settings_creationNew": "Creación (Nuevo Primero)",
"settings_creationOld": "Creación (Viejo Primero)",
"settings_name": "Nombre",
@ -86,7 +86,7 @@
"settings_defaultTaskColor": "Color de Tarea Predeterminado",
"settings_addCategory": "Agregar Categoría",
"settings_issueTrackers": "Seguimiento de Incidencias",
"settings_issueTrackersHelp": "Ejemplo URL: https://github.com/kiswa/TaskBoard/issues/%BUGID\\1% Ejemplo RegExp: (?:Issue)?#(\\d+)",
"settings_issueTrackersHelp": "Ejemplo URL: https://github.com/kiswa/TaskBoard/issues/%BUGID% Ejemplo RegExp: (?:Issue)?#(\\d+)",
"settings_issueTrakcerUrl": "URL de Seguimiento de Incidencia: Utilice %BUGID% como marcador de posición",
"settings_issueTrackerRegExp": "BUGID RegExp",
"settings_addIssueTracker": "Agregar Seguimiento de Incidencia",
@ -186,6 +186,13 @@
"boards_taskCategory": "Categoría",
"boards_task": "Tarea",
"boards_taskComplete": "Completar"
"boards_taskComplete": "Completo",
"boards_taskCopied": "copiado a tablero",
"boards_taskMoved": "movido a tablero",
"boards_sortByPosition": "Posición",
"boards_sortByDueDate": "Fecha de Vencimiento",
"boards_sortByLastModified": "Última Modificación",
"boards_sortByPoints": "Puntos"
}

View File

@ -39,7 +39,9 @@
@include clearfix();
background-color: #fff;
display: flex;
flex: 1 0 300px;
flex-direction: column;
height: calc(100% - 7px);
margin-left: 7px;
overflow: auto;
@ -98,6 +100,18 @@
z-index: 100;
}
.sort-by {
cursor: default;
float: right;
font-size: .6em;
}
select {
height: 1.7em;
padding: 0;
width: auto;
}
.icon-cancel,
.icon-floppy {
margin: 5px;
@ -139,11 +153,12 @@
&.collapsed {
background-color: $color-heading-bg;
display: block;
flex: 0 0 35px;
overflow: hidden;
h3 {
border: 0;
overflow: unset;
transform: rotate(90deg)
translateX(-35px)
translateY(5px);
@ -163,6 +178,7 @@
.icon-minus-squared-alt,
.icon-plus-squared-alt,
.tasks,
.sort-by,
.quick-add {
display: none;
}
@ -174,14 +190,9 @@
}
.tasks {
bottom: 0;
left: 0;
overflow-y: auto;
padding: 7px;
padding-top: 0;
position: absolute;
right: 0;
top: 5.2em;
div:last-of-type {
margin-bottom: 0;
@ -196,6 +207,20 @@
.task {
@include shadow-low();
a:link,
a:visited {
background-image: none;
color: inherit;
font-weight: bold;
text-decoration: underline;
text-shadow: none;
}
a:hover,
a:active {
text-decoration: none;
}
h4 {
border-bottom: 1px solid lighten($color-border, 25%);
cursor: move;

View File

@ -92,6 +92,27 @@ class TasksTest extends PHPUnit_Framework_TestCase {
$this->assertEquals('success', $actual->status);
}
/**
* @group single
*/
public function testAddTaskTop() {
$this->createTask();
$data = $this->getTaskData();
$user = R::load('user', 1);
$opts = R::load('useroption', $user->user_option_id);
$opts->new_tasks_at_bottom = false;
R::store($opts);
$request = new RequestMock();
$request->header = [DataMock::GetJwt()];
$request->payload = $data;
$actual = $this->tasks->addTask($request, new ResponseMock(), null);
$this->assertEquals('success', $actual->status);
}
public function testAddTaskUnprivileged() {
DataMock::CreateUnprivilegedUser();