WIP - Persist collapsed columns and start Task addition

This commit is contained in:
kiswa 2017-02-26 17:25:39 +00:00
parent f28ef733d6
commit ac6808ffa7
17 changed files with 277 additions and 44 deletions

View File

@ -206,6 +206,12 @@ class Auth extends BaseController {
$user = R::load('user', $payload->uid);
$opts = R::load('useroption', $user->user_option_id);
$collapsed = R::find('collapsed', ' user_id = ? ', [ $user->id ]);
$user->collapsed = [];
foreach ($collapsed as $collapse) {
$user->collapsed[] = $collapse->column_id;
}
$this->apiJson->setSuccess();
$this->apiJson->addData($jwt);

View File

@ -237,6 +237,43 @@ class Users extends BaseController {
return $this->jsonResponse($response);
}
public function toggleCollapsed($request, $response, $args) {
$status = $this->secureRoute($request, $response, SecurityLevel::USER);
if ($status !== 200) {
return $this->jsonResponse($response, $status);
}
$user = R::load('user', (int)$args['id']);
$actor = R::load('user', Auth::GetUserId($request));
if ($actor->id !== $user->id) {
$this->apiJson->addAlert('error', 'Access restricted.');
return $this->jsonResponse($response, 403);
}
$data = json_decode($request->getBody());
$collapsed = R::findOne('collapsed', ' user_id = ? AND column_id = ? ',
[ $user->id, $data->id ]);
if (!is_null($collapsed)) {
R::trash($collapsed);
} else {
$collapsed = R::dispense('collapsed');
$collapsed->user_id = $user->id;
$collapsed->column_id = $data->id;
R::store($collapsed);
}
$allCollapsed = R::find('collapsed', ' user_id = ? ', [ $user->id ]);
$this->apiJson->setSuccess();
$this->apiJson->addData(R::exportAll($allCollapsed));
return $this->jsonResponse($response);
}
public function removeUser($request, $response, $args) {
$status = $this->secureRoute($request, $response, SecurityLevel::ADMIN);
if ($status !== 200) {

View File

@ -46,17 +46,17 @@ class BeanLoader {
$board->is_active = isset($data->is_active) ? $data->is_active : '';
if (isset($data->categories)) {
self::updateBoardList('category', 'LoadCategory',
self::updateObjectList('category', 'LoadCategory',
$board->xownCategoryList, $data->categories);
}
if (isset($data->columns)) {
self::updateBoardList('column', 'LoadColumn',
self::updateObjectList('column', 'LoadColumn',
$board->xownColumnList, $data->columns);
}
if (isset($data->issue_trackers)) {
self::updateBoardList('issuetracker', 'LoadIssueTracker',
self::updateObjectList('issuetracker', 'LoadIssueTracker',
$board->xownIssueTrackerList,
$data->issue_trackers);
}
@ -106,6 +106,11 @@ class BeanLoader {
$column->position = isset($data->position) ? $data->position : '';
$column->board_id = isset($data->board_id) ? $data->board_id : '';
if (isset($data->tasks)) {
self::updateObjectList('task', 'LoadTask',
$column->xownTaskList, $data->tasks);
}
if (!isset($data->name) || !isset($data->position) ||
!isset($data->board_id)) {
return false;
@ -150,15 +155,32 @@ class BeanLoader {
$task->title = isset($data->title) ? $data->title : '';
$task->description = isset($data->description)
? $data->description : '';
$task->assignee = isset($data->assignee) ? $data->assignee : '';
$task->category_id = isset($data->category_id)
? $data->category_id : '';
$task->color = isset($data->color) ? $data->color : '';
$task->due_date = isset($data->due_date) ? $data->due_date : '';
$task->points = isset($data->points) ? $data->points : '';
$task->position = isset($data->position) ? $data->position : '';
$task->column_id = isset($data->column_id) ? $data->column_id : '';
if (isset($data->comments)) {
self::updateObjectList('comment', 'LoadComment',
$column->xownCommentList, $data->comments);
}
if (isset($data->attachments)) {
self::updateObjectList('attachment', 'LoadAttachment',
$column->xownAttachmentList, $data->attachments);
}
if (isset($data->assignees)) {
self::updateObjectList('user', 'LoadUser',
$column->xownAssigneeList, $data->assignees);
}
if (isset($data->categories)) {
self::updateObjectList('category', 'LoadCategory',
$column->xownCategoryList, $data->categories);
}
if (!isset($data->title) || !isset($data->position) ||
!isset($data->column_id)) {
return false;
@ -210,7 +232,7 @@ class BeanLoader {
return true;
}
private static function removeObjectsNotInData($type, &$dataList, &$boardList) {
private static function removeObjectsNotInData($type, &$dataList, &$objectList) {
$dataIds = [];
foreach ($dataList as $data) {
@ -219,7 +241,7 @@ class BeanLoader {
}
}
foreach ($boardList as $existing) {
foreach ($objectList as $existing) {
if (!in_array((int)$existing->id, $dataIds)) {
$remove = R::load($type, $existing->id);
R::trash($remove);
@ -228,36 +250,36 @@ class BeanLoader {
}
private static function loadObjectsFromData($type, $loadFunc, &$dataList,
&$boardList) {
&$objectList) {
foreach ($dataList as $obj) {
$object = R::load($type, (isset($obj->id) ? $obj->id : 0));
if ((int)$object->id === 0) {
call_user_func_array(array(__CLASS__, $loadFunc),
array(&$object, json_encode($obj)));
$boardList[] = $object;
$objectList[] = $object;
continue;
}
call_user_func_array(array(__CLASS__, $loadFunc),
array(&$boardList[$object->id],
array(&$objectList[$object->id],
json_encode($obj)));
}
}
private static function updateBoardList($type, $loadFunc,
&$boardList = [], &$dataList = []) {
if (count($boardList) && count($dataList)) {
self::removeObjectsNotInData($type, $dataList, $boardList);
private static function updateObjectList($type, $loadFunc,
&$objectList = [], &$dataList = []) {
if (count($objectList) && count($dataList)) {
self::removeObjectsNotInData($type, $dataList, $objectList);
}
if (count($dataList)) {
self::loadObjectsFromData($type, $loadFunc, $dataList, $boardList);
self::loadObjectsFromData($type, $loadFunc, $dataList, $objectList);
}
// Remove all objects from existing boardlist when none in datalist
if (!count($dataList) && count($boardList)) {
foreach ($boardList as $obj) {
if (!count($dataList) && count($objectList)) {
foreach ($objectList as $obj) {
R::trash($obj);
}
}

View File

@ -46,6 +46,7 @@ $app->get('/users/{id}', 'Users:getUser'); // User (by board access)
$app->post('/users', 'Users:addUser'); // Admin
$app->post('/users/{id}', 'Users:updateUser'); // User (limited to self - Higher can edit any)
$app->post('/users/{id}/opts', 'Users:updateUserOptions'); // User (limited to self)
$app->post('/users/{id}/cols', 'Users:toggleCollapsed'); // User (limited to self)
$app->delete('/users/{id}', 'Users:removeUser'); // Admin
$app->post('/login', 'Auth:login'); // Unsecured (creates JWT)

View File

@ -60,7 +60,6 @@
<div class="board" *ngIf="activeBoard">
<tb-column class="column"
*ngFor="let column of activeBoard.columns"
[column]="column"
[sideBySide]="userOptions.multiple_tasks_per_row"></tb-column>
[column]="column"></tb-column>
</div>

View File

@ -10,7 +10,6 @@ import {
Board,
Column,
User,
UserOptions,
InlineEdit,
Modal,
Notification,
@ -26,7 +25,6 @@ import { BoardService } from './board.service';
})
export class BoardDisplay implements OnInit {
private activeUser: User;
private userOptions: UserOptions;
private activeBoard: Board;
private boards: Array<Board>;
@ -60,7 +58,6 @@ export class BoardDisplay implements OnInit {
auth.userChanged.subscribe((user: User) => {
this.updateActiveUser(user);
this.userOptions = auth.userOptions;
});
active.params.subscribe(params => {
@ -122,6 +119,7 @@ export class BoardDisplay implements OnInit {
this.boards.forEach(board => {
if (board.id === this.boardNavId) {
this.activeBoard = board;
this.boardService.updateActiveBoard(board);
this.pageName = board.name;
}
});

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
@ -14,9 +15,17 @@ import {
@Injectable()
export class BoardService {
private activeBoard = new BehaviorSubject<Board>(null);
public activeBoardChanged = this.activeBoard.asObservable();
constructor(private http: Http) {
}
updateActiveBoard(board: Board): void {
this.activeBoard.next(board);
}
getBoards(): Observable<ApiResponse> {
return this.http.get('api/boards')
.map(res => {
@ -29,6 +38,20 @@ export class BoardService {
});
}
toggleCollapsed(userId: number, columnId: number): Observable<ApiResponse> {
return this.http.post('api/users/' + userId + '/cols',
{ id: columnId })
.map(res => {
let response: ApiResponse = res.json();
return response;
})
.catch((res, caught) => {
let response: ApiResponse = res.json();
return Observable.of(response);
});
}
// TODO: Determine when to use this
refreshToken(): void {
this.http.post('api/refresh', {}).subscribe();
}

View File

@ -67,8 +67,40 @@
</div>
</div>
<tb-modal modal-title="{{ modalProps.title }} Task" modal-id="{{ MODAL_ID }}">
<!-- TODO Move to Task HTML once created -->
<!--<tb-modal modal-title="Confirm Task Removal" blocking="true">-->
<!-- <div class="center">Removing a task cannot be undone.<br>Continue?</div>-->
<!-- <div class="buttons">-->
<!-- <button class="flat"-->
<!-- (click)="removeUser()">Yes</button>-->
<!-- <button #defaultAction-->
<!-- (click)="modal.close(MODAL_CONFIRM_ID)">No</button>-->
<!-- </div>-->
<!--</tb-modal>-->
<tb-modal modal-title="Add Task" modal-id="{{ MODAL_ID }}">
<label>
Title
<input type="text" name="title" placeholder="Task Title"
[(ngModel)]="modalProps.title">
</label>
<label>
Description
<textarea name="description" rows="5"
placeholder="What needs to get done?"
[(ngModel)]="modalProps.description"></textarea>
</label>
<label *ngIf="activeBoard">
Assignees
<select name="assignees" multiple [(ngModel)]="modalProps.assignees">
<option *ngFor="let user of activeBoard.users"
[ngValue]="user">{{ user.username }}</option>
</select>
</label>
<pre>{{ modalProps | json }}</pre>
<div class="buttons">
<button #defaultAction
(click)="addEditTask()" [disabled]="saving">

View File

@ -6,13 +6,19 @@ import {
} from '@angular/core';
import {
ApiResponse,
Board,
Column,
Modal,
Notification,
// Task, // TODO: Create Task model
Task,
User,
UserOptions,
AuthService,
ModalService,
NotificationsService
} from '../../shared/index';
import { BoardService } from '../board.service';
@Component({
selector: 'tb-column',
@ -20,38 +26,77 @@ import {
})
export class ColumnDisplay implements OnInit {
private templateElement: any;
private tasks: Array<any>; // TODO: Use Task model
private collapseTasks: boolean;
private modalProps: any; // TODO: Create ModalProperties model
private activeUser: User;
private activeBoard: Board;
private userOptions: UserOptions;
private tasks: Array<Task>;
private modalProps: Task;
private MODAL_ID: string;
private MODAL_CONFIRM_ID: string;
@Input('column') columnData: Column;
@Input('sideBySide') sideBySide: boolean;
constructor(private elRef: ElementRef,
private auth: AuthService,
private notes: NotificationsService,
private modal: ModalService) {
private modal: ModalService,
private boardService: BoardService) {
this.MODAL_ID = 'task-addEdit-form';
this.MODAL_CONFIRM_ID = 'task-remove-confirm';
this.templateElement = elRef.nativeElement;
this.tasks = [];
this.collapseTasks = false;
this.modalProps = { title: '' }; // TODO: Use model
this.modalProps = new Task();
boardService.activeBoardChanged.subscribe((board: Board) => {
this.activeBoard = board;
});
auth.userChanged.subscribe((user: User) => {
this.activeUser = new User(+user.default_board_id,
user.email,
+user.id,
user.last_login,
+user.security_level,
+user.user_option_id,
user.username,
user.board_access,
user.collapsed);
this.userOptions = auth.userOptions;
});
}
ngOnInit() {
this.templateElement.classList.remove('double');
if (this.sideBySide) {
if (this.userOptions.multiple_tasks_per_row) {
this.templateElement.classList.add('double');
}
let isCollapsed = false;
this.activeUser.collapsed.forEach(id => {
if (+id === +this.columnData.id) {
isCollapsed = true;
}
});
if (isCollapsed) {
this.templateElement.classList.add('collapsed');
}
}
toggleCollapsed() {
this.templateElement.classList.toggle('collapsed');
this.boardService.toggleCollapsed(this.activeUser.id, this.columnData.id)
.subscribe((apiResponse: ApiResponse) => {
this.activeUser.collapsed = apiResponse.data[1];
});
}
toggleTaskCollapse() {
@ -62,15 +107,8 @@ export class ColumnDisplay implements OnInit {
// TODO
}
private showModal(title: string, task?: any): void { // TODO: Use Task model
let isAdd = (title === 'Add');
this.modalProps = {
title,
prefix: isAdd ? '' : 'Edit',
task: isAdd ? task /*new Task()*/ : task
};
private showModal(): void {
this.modalProps = new Task();
this.modal.open(this.MODAL_ID);
}
}

View File

@ -1,9 +1,11 @@
import { Task } from './task.model';
export class Column {
constructor(public id: number = 0,
public name: string = '',
public position: number = 0,
public board_id: number = 0, // tslint:disable-line
public tasks: Array<any> = []) { // TODO: Use Task model
public tasks: Array<Task> = []) {
}
}

View File

@ -7,4 +7,5 @@ export * from './issue-tracker.model';
export * from './notification.model';
export * from './user-options.model';
export * from './user.model';
export * from './task.model';

View File

@ -0,0 +1,19 @@
import { Category } from './category.model';
import { User } from './user.model';
export class Task {
constructor(public id: number = 0,
public title: string = '',
public description: string = '',
public color: string = '',
public due_date: string = '', // tslint:disable-line
public points: number = 0,
public position: number = 0,
public column_id: number = 0, // tslint:disable-line
public comments: Array<any> = [], // TODO: Use model
public attachments: Array<any> = [], // TODO: Use model
public assignees: Array<User> = [],
public categories: Array<Category> = []) {
}
}

View File

@ -6,7 +6,8 @@ export class User {
public security_level: number = 3, // tslint:disable-line
public user_option_id: number = 0, // tslint:disable-line
public username: string = '',
public board_access: Array<number> = []) { // tslint:disable-line
public board_access: Array<number> = [], // tslint:disable-line
public collapsed: Array<number> = []) {
}
}

View File

@ -196,6 +196,11 @@ class AuthTest extends PHPUnit_Framework_TestCase {
$data->password = 'admin';
$data->remember = false;
$collapsed = R::dispense('collapsed');
$collapsed->user_id = 1;
$collapsed->column_id = 1;
R::store($collapsed);
$request = new RequestMock();
$request->payload = $data;

View File

@ -257,6 +257,7 @@ class ColumnsTest extends PHPUnit_Framework_TestCase {
$data->name = 'test';
$data->position = 0;
$data->board_id = 1;
$data->tasks = [];
return $data;
}

View File

@ -248,13 +248,15 @@ class TasksTest extends PHPUnit_Framework_TestCase {
$data->title = 'task';
$data->description = 'the words';
$data->assignee = 0;
$data->category_id = 0;
$data->column_id = 1;
$data->color = '';
$data->due_date = null;
$data->points = null;
$data->position = 0;
$data->comments = [];
$data->attachments = [];
$data->assignees = [];
$data->categories = [];
return $data;
}

View File

@ -408,6 +408,52 @@ class UsersTest extends PHPUnit_Framework_TestCase {
$this->assertEquals('failure', $response->status);
}
public function testToggleCollapsed() {
$this->createUser();
$data = new stdClass();
$data->id = 1;
$args = [];
$args['id'] = 2;
$request = new RequestMock();
$request->payload = $data;
$request->header = [DataMock::GetJwt(2)];
// Collapse the column
$response = $this->users->toggleCollapsed($request,
new ResponseMock(), $args);
$this->assertEquals('success', $response->status);
// Expand the column
$response = $this->users->toggleCollapsed($request,
new ResponseMock(), $args);
$this->assertEquals('success', $response->status);
}
public function testToggleCollapsedNoAccess() {
$response = $this->users->toggleCollapsed(new RequestMock(),
new ResponseMock(), null);
$this->assertEquals('failure', $response->status);
$data = new stdClass();
$data->id = 1;
$args = [];
$args['id'] = 2;
$request = new RequestMock();
$request->payload = $data;
$request->header = [DataMock::GetJwt(1)];
$response = $this->users->toggleCollapsed($request,
new ResponseMock(), $args);
$this->assertEquals('failure', $response->status);
}
public function testRemoveUser() {
$this->createUser();