Rework context menu behavior

This commit is contained in:
kiswa 2017-08-20 23:44:24 +00:00
parent f3c4af20a6
commit 8ff331c683
15 changed files with 4597 additions and 3572 deletions

7929
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,24 +27,24 @@
},
"homepage": "https://github.com/kiswa/TaskBoard#readme",
"devDependencies": {
"@angular/common": "4.3.2",
"@angular/compiler": "4.3.2",
"@angular/core": "4.3.2",
"@angular/forms": "4.3.2",
"@angular/http": "4.3.2",
"@angular/platform-browser": "4.3.2",
"@angular/platform-browser-dynamic": "4.3.2",
"@angular/router": "4.3.2",
"@angular/common": "4.3.5",
"@angular/compiler": "4.3.5",
"@angular/core": "4.3.5",
"@angular/forms": "4.3.5",
"@angular/http": "4.3.5",
"@angular/platform-browser": "4.3.5",
"@angular/platform-browser-dynamic": "4.3.5",
"@angular/router": "4.3.5",
"@types/chartist": "^0.9.35",
"@types/core-js": "^0.9.42",
"@types/highlight.js": "^9.1.9",
"@types/marked": "0.0.28",
"@types/marked": "0.3.0",
"bourbon": "^4.3.4",
"bourbon-neat": "1.9.0",
"chai": "^4.1.0",
"chai": "^4.1.1",
"chartist": "^0.11.0",
"chartist-plugin-tooltip": "git+https://github.com/Globegitter/chartist-plugin-tooltip.git",
"core-js": "^2.4.1",
"chartist-plugin-tooltips": "^0.0.17",
"core-js": "^2.5.0",
"del": "^3.0.0",
"dragula": "^3.7.2",
"escape-string-regexp": "^1.0.5",
@ -53,7 +53,7 @@
"gulp-chmod": "^2.0.0",
"gulp-composer": "^0.4.0",
"gulp-concat": "^2.6.1",
"gulp-cssimport": "^5.0.0",
"gulp-cssimport": "^5.1.1",
"gulp-cssnano": "^2.1.2",
"gulp-imagemin": "^3.3.0",
"gulp-istanbul": "^1.1.2",
@ -61,7 +61,7 @@
"gulp-phpunit": "^0.23.0",
"gulp-sass": "^3.1.0",
"gulp-scss-lint": "^0.5.0",
"gulp-tslint": "^8.1.1",
"gulp-tslint": "^8.1.2",
"gulp-typescript": "^3.2.1",
"gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.8",
@ -72,12 +72,12 @@
"ng2-dragula": "^1.5.0",
"node-normalize-scss": "^3.0.0",
"reflect-metadata": "^0.1.10",
"rxjs": "5.4.2",
"rxjs": "5.4.3",
"scss-base": "^1.3.4",
"systemjs": "0.20.17",
"systemjs": "0.20.18",
"systemjs-builder": "0.16.9",
"touch": "^3.1.0",
"tslint": "^5.5.0",
"tslint": "^5.6.0",
"typescript": "2.4.2",
"zone.js": "^0.8.16"
}

View File

@ -59,7 +59,7 @@ class Tasks extends BaseController {
R::store($task);
$actor = R::load('user', Auth::GetUserId($request));
$this->updateTaskOrder($task, $actor);
$this->updateTaskOrder($task, $actor, true);
$this->dbLogger->logChange($actor->id,
$actor->username . ' added task ' . $task->title . '.',
@ -106,6 +106,8 @@ class Tasks extends BaseController {
R::store($update);
$actor = R::load('user', Auth::GetUserId($request));
$this->updateTaskOrder($task, $actor, false);
$this->dbLogger->logChange($actor->id,
$actor->username . ' updated task ' . $task->title,
json_encode($task), json_encode($update),
@ -151,6 +153,8 @@ class Tasks extends BaseController {
R::trash($task);
$actor = R::load('user', Auth::GetUserId($request));
$this->updateTaskOrder($task, $actor, false);
$this->dbLogger->logChange($actor->id,
$actor->username . ' removed task ' . $before->title,
json_encode($before), '', 'task', $id);
@ -171,7 +175,7 @@ class Tasks extends BaseController {
return $column->board_id;
}
private function updateTaskOrder($task, $user) {
private function updateTaskOrder($task, $user, $isNew) {
$column = R::load('column', $task->column_id);
$user_opts = R::load('useroption', $user->user_option_id);
@ -181,8 +185,9 @@ class Tasks extends BaseController {
$counter++;
}
if ($user_opts->new_tasks_at_bottom) {
R::store($column);
R::store($column);
if (!$isNew || $user_opts->new_tasks_at_bottom) {
return;
}

View File

@ -12,6 +12,7 @@ import { API_HTTP_PROVIDERS } from './app.api-http';
import { Constants } from './shared/constants';
import {
AuthGuard,
CompileDirective,
ContextMenu,
InlineEdit,
Modal,
@ -47,8 +48,9 @@ import { BoardService } from './board/board.service';
],
declarations: [
AppComponent,
InlineEdit,
CompileDirective,
ContextMenu,
InlineEdit,
Modal,
Notifications,
TopNav,

View File

@ -1,7 +1,7 @@
<h3 [class.near-limit]="columnData.hasTaskLimit() &&
columnData.task_limit - columnData.tasks.length === 1"
columnData.task_limit - columnData.tasks.length === 0"
[class.limit-reached]="columnData.hasTaskLimit() &&
columnData.task_limit - columnData.tasks.length < 1">
columnData.task_limit - columnData.tasks.length < 0">
<span class="icon icon-minus-squared-alt"
[title]="strings['boards_collapseAllTasks']"
*ngIf="!collapseTasks"
@ -44,6 +44,7 @@
(click)="saveLimitChanges()"></i>
</div>
</span>
<span class="sort-by">
{{ strings['sortBy'] }}:
<select [(ngModel)]="sortOption"

View File

@ -20,6 +20,7 @@
<div class="description" *ngIf="!isCollapsed"
[innerHTML]="getTaskDescription()"></div>
<pre>{{ taskData.position | json }}</pre>
<div class="stats">
<span *ngIf="userOptions.show_assignee">
@ -33,7 +34,9 @@
</span>
<span class="right">
<span *ngIf="taskData.due_date">
<span *ngIf="taskData.due_date"
[class.overdue]="isOverdue"
[class.near-due]="isNearlyDue">
{{ strings['boards_taskDue'] }}: {{ taskData.due_date }}
</span>
<span *ngIf="taskData.comments.length" class="icon icon-chat-empty"

View File

@ -34,7 +34,6 @@ export class TaskDisplay implements OnInit {
private strings: any;
private userOptions: UserOptions;
private contextMenuItems: Array<ContextMenuItem>;
private selectMenuItem: ContextMenuItem;
private activeBoard: Board;
private boardsList: Array<Board>;
@ -43,6 +42,9 @@ export class TaskDisplay implements OnInit {
private completeTasks: number;
private percentComplete: number;
private isOverdue: boolean;
private isNearlyDue: boolean;
@Input('task') taskData: Task;
@Input('add-task') addTask: Function;
@Input('edit-task') editTask: Function;
@ -98,6 +100,7 @@ export class TaskDisplay implements OnInit {
this.generateContextMenuItems();
this.initMarked();
this.calcPercentComplete();
this.checkDueDate();
}
getTaskDescription(): SafeHtml {
@ -129,6 +132,32 @@ export class TaskDisplay implements OnInit {
return yiq >= 140 ? '#333333' : '#efefef';
}
private checkDueDate() {
if (this.taskData.due_date === '') {
return;
}
let dueDate = new Date(this.taskData.due_date);
if (isNaN(dueDate.valueOf())) {
return;
}
let millisecondsPerDay = (1000 * 3600 * 24),
today = new Date(),
timeDiff = today.getTime() - dueDate.getTime(),
daysDiff = Math.ceil(timeDiff / millisecondsPerDay);
if (daysDiff > 0) {
// past due date
this.isOverdue = true;
}
if (daysDiff <= 0 && daysDiff > -3) {
this.isNearlyDue = true;
}
}
// Needs anonymous function for proper `this` context.
private markedCallback = (error: any, text: string) => {
this.activeBoard.issue_trackers.forEach(tracker => {
@ -161,7 +190,9 @@ export class TaskDisplay implements OnInit {
private getMoveMenuItem() {
let menuText = this.strings.boards_moveTask +
': <select id="columnsList' + this.taskData.id + '">';
': <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>';
@ -177,9 +208,7 @@ export class TaskDisplay implements OnInit {
this.changeTaskColumn();
};
return new ContextMenuItem(menuText,
action,
false, false);
return new ContextMenuItem(menuText, action, false, false, true);
}
private changeTaskColumn() {
@ -237,8 +266,10 @@ export class TaskDisplay implements OnInit {
private getMenuItem(text: string): ContextMenuItem {
let menuText = text + ': ' +
'<i class="icon icon-help-circled" ' +
'data-help="' + this.strings.boards_copyMoveHelp + '"></i> ' +
'<select id="boardsList' + text.split(' ')[0] + '">';
'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) {
@ -261,11 +292,11 @@ export class TaskDisplay implements OnInit {
this.moveTaskToBoard();
};
return new ContextMenuItem(menuText, action, false, false);
return new ContextMenuItem(menuText, action, false, false, true);
}
private copyTaskToBoard() {
let select = document.getElementById('boardsList' +
let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_copyTaskTo.split(' ')[0]) as HTMLSelectElement;
let newBoardId = +select[select.selectedIndex].value;
@ -298,7 +329,7 @@ export class TaskDisplay implements OnInit {
}
private moveTaskToBoard() {
let select = document.getElementById('boardsList' +
let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement;
let newBoardId = +select[select.selectedIndex].value;

View File

@ -0,0 +1,84 @@
import {
Compiler,
Component,
ComponentRef,
Directive,
Input,
ModuleWithComponentFactories,
NgModule,
OnChanges,
Type,
ViewContainerRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[compile]'
})
export class CompileDirective implements OnChanges {
@Input() compile: string;
@Input() compileContext: any;
compRef: ComponentRef<any>;
constructor(private vcRef: ViewContainerRef,
private compiler: Compiler) { }
ngOnChanges() {
if (!this.compile) {
if (this.compRef) {
this.updateProperties();
return;
}
throw Error('You forgot to provide template');
}
this.vcRef.clear();
this.compRef = null;
const component = this.createDynamicComponent(this.compile);
const module = this.createDynamicModule(component);
this.compiler.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
let compFactory = moduleWithFactories.componentFactories
.find(x => x.componentType === component);
this.compRef = this.vcRef.createComponent(compFactory);
this.updateProperties();
})
.catch(error => {
console.log(error); // tslint:disable-line
});
}
updateProperties() {
for (let prop in this.compileContext) {
if (this.compileContext.hasOwnProperty(prop)) {
this.compRef.instance[prop] = this.compileContext[prop];
}
}
}
private createDynamicComponent (template: string) {
@Component({
selector: 'custom-dynamic-component',
template
})
class CustomDynamicComponent {}
return CustomDynamicComponent;
}
private createDynamicModule (component: Type<any>) {
@NgModule({
imports: [CommonModule],
declarations: [component]
})
class DynamicModule {}
return DynamicModule;
}
}

View File

@ -2,10 +2,16 @@ export class ContextMenuItem {
constructor(public text: string = '',
public action: Function = null,
public isSeparator: boolean = false,
public canHighlight: boolean = true) {
public canHighlight: boolean = true,
public isCustom: boolean = false) {
if (isSeparator) {
text = '<hr>';
canHighlight = false;
this.text = '<hr>';
this.canHighlight = false;
}
if (!isCustom) {
this.text = '<span (click)="action($event)">' + this.text +
'</span>';
}
}
}

View File

@ -1,10 +1,12 @@
<div class="context-menu-container"
[ngClass]="{ 'menu-open': isOpen }">
[ngClass]="{ 'menu-open': isOpen }"
(click)="$event.stopPropagation()">
<div class="menu-item" *ngFor="let item of menuItems"
[ngClass]="{ 'no-highlight': item.isSeparator || !item.canHighlight }"
(click)="callAction($event, item.action)"
[innerHTML]="getText(item)">
(click)="menuService.closeAllMenus()"
[ngClass]="{ 'no-highlight': item.isSeparator || !item.canHighlight }">
<ng-template
*compile="item.text; context: { action: item.action }"></ng-template>
</div>
</div>

View File

@ -33,15 +33,6 @@ export class ContextMenu {
return this.sanitizer.bypassSecurityTrustHtml(item.text);
}
callAction(event: MouseEvent, action: Function) {
console.log(event); // tslint:disable-line
if (action) {
action(event);
}
this.menuService.closeAllMenus();
}
private captureChildEvents(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();

View File

@ -1,5 +1,6 @@
export * from './constants';
export * from './auth/index';
export * from './compile/compile.directive';
export * from './constants';
export * from './context-menu/index';
export * from './inline-edit/inline-edit.component';
export * from './modal/index';

View File

@ -131,6 +131,7 @@
"settings_noActionsAdmin": "There are no automatic actions. Use the <strong>Add Action</strong> form below to add one.",
"boards_selectBoard": "Select Board",
"boards_selectColumn": "Select Column",
"boards_hideFiltered": "Hide filtered items",
"boards_userFilter": "User Filter",
"boards_categoryFilter": "Category Filter",

View File

@ -131,6 +131,7 @@
"settings_noActionsAdmin": "No hay acciones automáticas. Utilice el botón <strong>Agregar Acción</strong> de abajo para agregar uno.",
"boards_selectBoard": "Seleccionar el Tablero",
"boards_selectColumn": "Seleccionar la Columna",
"boards_hideFiltered": "Ocultar los elementos filtrados",
"boards_userFilter": "Filtro de usuario",
"boards_categoryFilter": "Filtro de categoría",

View File

@ -280,6 +280,14 @@
font-weight: 700;
}
.overdue {
text-shadow: 0 0 2px rgba(255, 0, 0, 1);
}
.near-due {
text-shadow: 0 0 2px rgba(255, 180, 0, 1);
}
&.compact {
.description {
display: none;