TSK-693 Query tasks by primary object reference

This commit is contained in:
Jose Ignacio Recuerda Cambil 2018-11-09 08:42:56 +01:00 committed by Martin Rojas Miguel Angel
parent 0bd103cb20
commit 955ea9e5f0
14 changed files with 325 additions and 138 deletions

View File

@ -56,6 +56,7 @@ public class TaskController extends AbstractPagingController {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskController.class);
private static final String LIKE = "%";
private static final String STATE = "state";
private static final String STATE_VALUE_CLAIMED = "CLAIMED";
private static final String STATE_VALUE_COMPLETED = "COMPLETED";
@ -277,13 +278,11 @@ public class TaskController extends AbstractPagingController {
params.remove(POR_SYSTEM_INSTANCE);
}
if (params.containsKey(POR_TYPE)) {
String[] types = extractCommaSeparatedFields(params.get(POR_TYPE));
taskQuery.primaryObjectReferenceTypeIn(types);
taskQuery.primaryObjectReferenceTypeLike(LIKE + params.get(POR_TYPE).get(0) + LIKE);
params.remove(POR_TYPE);
}
if (params.containsKey(POR_VALUE)) {
String[] values = extractCommaSeparatedFields(params.get(POR_VALUE));
taskQuery.primaryObjectReferenceValueIn(values);
taskQuery.primaryObjectReferenceValueLike(LIKE + params.get(POR_VALUE).get(0) + LIKE);
params.remove(POR_VALUE);
}
return taskQuery;

View File

@ -9,61 +9,78 @@ import { ClassificationDetailsComponent } from 'app/administration/classificatio
import { DomainGuard } from 'app/guards/domain-guard';
import { AccessItemsManagementComponent } from './access-items-management/access-items-management.component';
const routes: Routes = [
{
path: 'workbaskets',
component: MasterAndDetailComponent,
canActivate: [DomainGuard],
children: [
{
path: '',
component: WorkbasketListComponent,
outlet: 'master'
},
{
path: 'new-classification/:id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
},
{
path: ':id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
}
]
},
{
path: 'classifications',
component: MasterAndDetailComponent,
canActivate: [DomainGuard],
children: [
{
path: '',
component: ClassificationListComponent,
outlet: 'master'
},
{
path: ':id',
component: ClassificationDetailsComponent,
outlet: 'detail'
}
]
},
{
path: 'access-items-management',
component: AccessItemsManagementComponent,
canActivate: [DomainGuard]
},
{
{
path: 'workbaskets',
component: MasterAndDetailComponent,
canActivate: [DomainGuard],
children: [
{
path: '',
redirectTo: 'workbaskets',
pathMatch: 'full'
}
component: WorkbasketListComponent,
outlet: 'master'
},
{
path: 'new-classification/:id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
},
{
path: ':id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
},
{
path: '**',
redirectTo: ''
}
]
},
{
path: 'classifications',
component: MasterAndDetailComponent,
canActivate: [DomainGuard],
children: [
{
path: '',
component: ClassificationListComponent,
outlet: 'master'
},
{
path: ':id',
component: ClassificationDetailsComponent,
outlet: 'detail'
},
{
path: '**',
redirectTo: ''
}
]
},
{
path: 'access-items-management',
component: AccessItemsManagementComponent,
canActivate: [DomainGuard],
children: [
{
path: '**',
redirectTo: ''
}
]
},
{
path: '',
redirectTo: 'workbaskets',
pathMatch: 'full'
},
{
path: '**',
redirectTo: 'workbaskets'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdministrationRoutingModule { }
export class AdministrationRoutingModule {}

View File

@ -1,16 +1,20 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MonitorComponent } from './monitor.component'
import { MonitorComponent } from './monitor.component';
const routes: Routes = [
{
path: '',
component: MonitorComponent
{
path: '',
component: MonitorComponent
},
{
path: '**',
redirectTo: ''
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MonitorRoutingModule { }
export class MonitorRoutingModule {}

View File

@ -1,9 +1,11 @@
.sortby-dropdown {
min-width: 200px;
min-width: 200px;
}
ul {
list-style-type: none;
}
.bold-blue {
color: #337ab7;
font-weight: bold;
}

View File

@ -16,6 +16,8 @@ export class TaskanaQueryParameters {
static PRIORITY = 'priority';
static STATELIKE = 'state-like';
static WORKBASKET_ID = 'workbasket-id';
static TASK_PRIMARY_OBJ_REF_TYPE_LIKE = 'por.type';
static TASK_PRIMARY_OBJ_REF_VALUE_LIKE = 'por.value';
// Access
static REQUIREDPERMISSION = 'required-permission';
@ -53,6 +55,8 @@ export class TaskanaQueryParameters {
basketId: string = undefined,
priority: string = undefined,
stateLike: string = undefined,
objRefTypeLike: string = undefined,
objRefValueLike: string = undefined,
): string {
let query = '?';
query += sortBy ? `${this.SORTBY}=${sortBy}&` : '';
@ -79,6 +83,8 @@ export class TaskanaQueryParameters {
query += workbasketKeyLike
? `${this.WORKBASKETKEYLIKE}=${workbasketKeyLike}&`
: '';
query += objRefTypeLike ? `${this.TASK_PRIMARY_OBJ_REF_TYPE_LIKE}=${objRefTypeLike}&` : '';
query += objRefValueLike ? `${this.TASK_PRIMARY_OBJ_REF_VALUE_LIKE}=${objRefValueLike}&` : '';
if (query.lastIndexOf('&') === query.length - 1) {
query = query.slice(0, query.lastIndexOf('&'));

View File

@ -65,11 +65,13 @@ export class TaskService {
ownerLike: string,
priority: string,
state: string,
objRefTypeLike: string,
objRefValueLike: string,
allPages: boolean = false): Observable<TaskResource> {
const url = `${this.url}${TaskanaQueryParameters.getQueryParameters(
sortBy, sortDirection, undefined, nameLike, undefined, undefined, ownerLike, undefined, undefined, undefined, undefined,
!allPages ? TaskanaQueryParameters.page : undefined, !allPages ? TaskanaQueryParameters.pageSize : undefined,
undefined, undefined, undefined, undefined, basketId, priority, state)}`;
undefined, undefined, undefined, undefined, basketId, priority, state, objRefTypeLike, objRefValueLike)}`;
return this.httpClient.get<TaskResource>(url);
}

View File

@ -1,13 +1,17 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/models/workbasket';
import { ObjectReference } from '../models/object-reference';
@Injectable()
export class WorkplaceService {
// necessary because the TaskdetailsComponent is not always initialized when the first workbasket was selected.
currentWorkbasket: Workbasket;
objectReference: ObjectReference
private workbasketSelectedSource = new Subject<Workbasket>();
workbasketSelectedStream = this.workbasketSelectedSource.asObservable();
private objectReferenceSource = new Subject<ObjectReference>();
objectReferenceSelectedStream = this.objectReferenceSource.asObservable();
selectWorkbasket(workbasket: Workbasket) {
this.currentWorkbasket = workbasket;
@ -17,4 +21,17 @@ export class WorkplaceService {
getSelectedWorkbasket(): Observable<Workbasket> {
return this.workbasketSelectedStream;
}
selectObjectReference(objectReference: ObjectReference) {
if (objectReference) {
this.objectReference = new ObjectReference(undefined, undefined, undefined, undefined, objectReference.type, objectReference.value);
} else {
this.objectReference = new ObjectReference(undefined);
}
this.objectReferenceSource.next(objectReference);
}
getObjectReference() {
return this.objectReferenceSelectedStream;
}
}

View File

@ -1,23 +1,67 @@
<li id="tasklist-action-toolbar" class="list-group-item tab-align">
<div class="row">
<div *ngIf="currentBasket" class="col-xs-2">
<button (click)="createTask()" type="button" class="btn btn-default pull-left green-blue" title="Add Task to current Workbasket">
<div *ngIf="searched" class="col-xs-2">
<button (click)="createTask()" type="button" class="btn btn-default pull-left green-blue" data-toggle="tooltip" title="Add Task to current Workbasket">
<span class="material-icons md-20 green-blue">add_circle_outline</span>
</button>
</div>
<div class="col-xs-6">
<input [(ngModel)]="resultName" [typeahead]="workbasketNames" class="form-control" (typeaheadOnSelect)="searchBasket()"
(typeaheadNoResults)="workbasketSelected = false" placeholder="Search for Workbasket ..." />
<div [ngClass]="{'col-xs-8': !searched, 'col-xs-5': searched}">
<input [(ngModel)]="resultName" *ngIf="searchSelected === search.byWorkbasket" [typeahead]="workbasketNames"
class="form-control" (typeaheadOnSelect)="searchBasket()" (typeaheadNoResults)="workbasketSelected = false"
placeholder="Search for Workbasket ..." />
<div class="input-group" style="margin-bottom: 1px;" *ngIf="searchSelected === search.byTypeAndValue">
<input [(ngModel)]="resultType" class="form-control" (keyup.enter)="searchBasket()" placeholder="Search by type ..." />
<span _ngcontent-c10="" class="input-group-btn">
<button _ngcontent-c10="" class="btn btn-primary" style="height: 34px;" type="button" (click)="searchBasket()">
<span class="material-icons md-20 white">search</span>
</button>
</span>
</div>
<input [(ngModel)]="resultValue" *ngIf="searchSelected === search.byTypeAndValue" class="form-control" (keyup.enter)="searchBasket()"
placeholder="Search by value ..." />
</div>
<div *ngIf="currentBasket" class="pull-right margin-right btn-group">
<div *ngIf="searched" class="pull-right margin-right btn-group">
<taskana-sort [sortingFields]="sortingFields" (performSorting)="sorting($event)" class="btn-group"></taskana-sort>
<button class="btn btn-default collapsed" type="button" id="collapsedMenufilterWb" aria-expanded="false" (click)="toolbarState=!toolbarState">
<span class="material-icons md-20 blue">{{!toolbarState? 'search' : 'expand_less'}}</span>
<span class="material-icons md-20 blue">search</span>
</button>
</div>
<div class="dropdown clearfix pull-right margin-right">
<button type="button" class="btn btn-default dropdown-button" [ngClass]="{'reduced-width': searched}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" title="{{searchSelected == search.byTypeAndValue ? 'Type and value' : 'Workbasket'}}">
<svg-icon *ngIf="searchSelected == search.byWorkbasket" class="blue" src="./assets/icons/wb-empty.svg"></svg-icon>
<span *ngIf="searchSelected == search.byTypeAndValue" class="material-icons md-20 blue">title</span>
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu" aria-labelledby="dropdownMenu">
<div class="dropdown">
<h5 class="bold-blue align-center">Search by</h5>
</div>
<div role="separator" class="divider"></div>
<ul style="padding: 15px;">
<li data-toggle="tooltip" title="Workbasket">
<a class="dropdown-item" (click)="selectSearch(search.byWorkbasket)">
<label>
<span class="material-icons md-20 blue margin-right" aria-hidden="true">{{ searchSelected === search.byWorkbasket ? 'check_box': 'check_box_outline_blank' }}</span>
<svg-icon class="margin-right" src="./assets/icons/wb-empty.svg"></svg-icon>
<small class="margin-right blue-font" style="margin-left: 4px;">Workbasket</small>
</label>
</a>
</li>
<li data-toggle="tooltip" title="Type and value">
<a class="dropdown-item" (click)="selectSearch(search.byTypeAndValue)">
<label>
<span class="material-icons md-20 blue" aria-hidden="true">{{ searchSelected === search.byTypeAndValue ? 'check_box': 'check_box_outline_blank' }}</span>
<span class="material-icons md-20 blue">title</span>
<small class="blue-font">Type and value</small>
</label>
</a>
</li>
</ul>
</div>
</div>
</div>
<div [@toggleDown]="toolbarState" class="row no-overflow">
<taskana-filter [filterParams]="filterParams" [filterType]="filterType" (performFilter)="filtering($event)">
</taskana-filter>
</div>
</li>
</li>

View File

@ -1,13 +1,46 @@
@import './src/assets/_colors';
.list-group-item {
padding: 5px 0px 2px 1px;
border: none;
}
.tab-align{
.tab-align {
padding: 8px 12px 8px 0px;
margin-bottom: 0px;
&>div{
& > div {
margin: 6px 0px;
}
}
ul {
list-style-type: none;
}
.blue-font {
color: $blue;
}
.bold-blue {
font-weight: bold;
color: $blue;
}
svg-icon {
height: 18px;
width: 18px;
fill: $blue;
}
svg-icon.blue {
fill: $blue;
}
@media screen and (min-width: 991px) and (max-width: 1115px) {
.reduced-width {
width: 37px;
padding-left: 5px;
padding-right: 5px;
}
}

View File

@ -7,9 +7,14 @@ import { SortingModel } from 'app/models/sorting';
import { FilterModel } from 'app/models/filter';
import { TaskanaType } from 'app/models/taskana-type';
import { expandDown } from 'app/shared/animations/expand.animation';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, NavigationExtras } from '@angular/router';
import { WorkplaceService } from 'app/workplace/services/workplace.service';
import { ObjectReference } from 'app/workplace/models/object-reference';
export enum Search {
byWorkbasket = 'workbasket',
byTypeAndValue = 'type-and-value'
}
@Component({
selector: 'taskana-tasklist-toolbar',
animations: [expandDown],
@ -20,6 +25,7 @@ export class TaskListToolbarComponent implements OnInit {
@Output() performSorting = new EventEmitter<SortingModel>();
@Output() performFilter = new EventEmitter<FilterModel>();
@Output() selectSearchType = new EventEmitter();
sortingFields = new Map([['name', 'Name'], ['priority', 'Priority'], ['due', 'Due'], ['planned', 'Planned']]);
@ -34,6 +40,13 @@ export class TaskListToolbarComponent implements OnInit {
workbasketSelected = false;
toolbarState = false;
filterType = TaskanaType.TASKS;
searched = false;
searchParam = 'search';
search = Search;
searchSelected: Search = Search.byWorkbasket;
resultType = '';
resultValue = '';
constructor(private taskService: TaskService,
private workbasketService: WorkbasketService,
@ -57,26 +70,42 @@ export class TaskListToolbarComponent implements OnInit {
this.workplaceService.selectWorkbasket(this.currentBasket);
this.workbasketSelected = true;
}
})
});
if (this.route.snapshot.queryParams.search === this.search.byTypeAndValue) {
this.searchSelected = this.search.byTypeAndValue;
}
if (this.router.url.includes('taskdetail')) {
this.searched = true;
}
}
searchBasket() {
this.toolbarState = false;
this.workbasketSelected = true;
if (this.workbaskets) {
this.workbaskets.forEach(workbasket => {
if (workbasket.name === this.resultName) {
this.resultId = workbasket.workbasketId;
this.currentBasket = workbasket;
this.workplaceService.selectWorkbasket(this.currentBasket);
}
});
if (this.searchSelected === this.search.byTypeAndValue) {
this.workplaceService.selectObjectReference(
new ObjectReference(undefined, undefined, undefined, undefined, this.resultType, this.resultValue));
this.searched = true;
} else {
this.workplaceService.selectObjectReference(undefined);
if (this.workbaskets) {
this.workbaskets.forEach(workbasket => {
if (workbasket.name === this.resultName) {
this.resultId = workbasket.workbasketId;
this.currentBasket = workbasket;
this.searched = true;
this.workplaceService.selectWorkbasket(this.currentBasket);
}
});
if (!this.resultId) {
this.currentBasket = undefined;
this.workplaceService.selectWorkbasket(undefined);
if (!this.resultId) {
this.currentBasket = undefined;
this.workplaceService.selectWorkbasket(undefined);
}
}
}
this.resultId = '';
this.router.navigate(['']);
}
@ -93,4 +122,18 @@ export class TaskListToolbarComponent implements OnInit {
this.taskService.selectTask(undefined);
this.router.navigate([{ outlets: { detail: 'taskdetail/new-task' } }], { relativeTo: this.route });
}
selectSearch(type: Search) {
this.searched = false;
this.resultId = undefined;
this.currentBasket = undefined;
this.selectSearchType.emit(type);
this.searchSelected = type;
const navigationExtras: NavigationExtras = {
queryParams: { search: type }
};
this.router.navigate([''], navigationExtras);
}
}

View File

@ -1,7 +1,7 @@
<div class="footer-space-pagination-list">
<div #wbToolbar>
<taskana-tasklist-toolbar (performSorting)="performSorting($event)" (performFilter)="performFilter($event)"
(importSucessful)="refreshWorkbasketList()">
(selectSearchType)="selectSearchType($event)" (importSucessful)="refreshWorkbasketList()">
</taskana-tasklist-toolbar>
</div>
<div *ngIf="!requestInProgress">
@ -45,4 +45,4 @@
</div>
</div>
<taskana-pagination *ngIf="tasks && tasks.length > 0" [(page)]="tasksPageInformation" [type]="type" (changePage)="changePage($event)"></taskana-pagination>
<taskana-code></taskana-code>
<taskana-code></taskana-code>

View File

@ -14,6 +14,8 @@ import { OrientationService } from 'app/services/orientation/orientation.service
import { Orientation } from 'app/models/orientation';
import { Page } from 'app/models/page';
import { TaskanaDate } from 'app/shared/util/taskana.date';
import { ObjectReference } from '../models/object-reference';
import { Search } from './tasklist-toolbar/tasklist-toolbar.component';
@Component({
selector: 'taskana-task-list',
@ -38,6 +40,8 @@ export class TasklistComponent implements OnInit, OnDestroy {
workbasketKey: ''
});
requestInProgress = false;
objectReference: ObjectReference;
selectedSearchType: Search = Search.byWorkbasket;
@ViewChild('wbToolbar')
private toolbarElement: ElementRef;
@ -46,6 +50,7 @@ export class TasklistComponent implements OnInit, OnDestroy {
private taskAddedSubscription: Subscription;
private workbasketChangeSubscription: Subscription;
private orientationSubscription: Subscription;
private objectReferenceSubscription: Subscription;
constructor(private router: Router,
private route: ActivatedRoute,
@ -69,12 +74,21 @@ export class TasklistComponent implements OnInit, OnDestroy {
});
this.workbasketChangeSubscription = this.workplaceService.workbasketSelectedStream.subscribe(workbasket => {
this.currentBasket = workbasket;
this.getTasks();
if (this.selectedSearchType === Search.byWorkbasket) {
this.getTasks();
}
});
this.taskAddedSubscription = this.taskService.taskAddedStream.subscribe(task => {
this.getTasks();
this.selectedId = task.taskId;
});
this.objectReferenceSubscription = this.workplaceService.objectReferenceSelectedStream.subscribe(objectReference => {
this.objectReference = objectReference;
this.currentBasket = undefined;
if (objectReference) {
this.getTasks();
}
});
}
ngOnInit() {
@ -83,7 +97,6 @@ export class TasklistComponent implements OnInit, OnDestroy {
if (!this.currentBasket) {
this.selectedId = task.taskId;
this.currentBasket = task.workbasketSummaryResource;
this.getTasks();
}
if (!task) {
this.selectedId = undefined;
@ -95,6 +108,7 @@ export class TasklistComponent implements OnInit, OnDestroy {
}
selectTask(taskId: string) {
this.workplaceService.selectObjectReference(undefined);
this.selectedId = taskId;
this.router.navigate([{ outlets: { detail: `taskdetail/${this.selectedId}` } }], { relativeTo: this.route });
}
@ -109,6 +123,11 @@ export class TasklistComponent implements OnInit, OnDestroy {
this.getTasks();
}
selectSearchType(type: Search) {
this.selectedSearchType = type;
this.tasks = [];
}
changePage(page) {
TaskanaQueryParameters.page = page;
this.getTasks();
@ -120,7 +139,7 @@ export class TasklistComponent implements OnInit, OnDestroy {
}
calculateHeightCard() {
if (this.toolbarElement && this.currentBasket) {
if (this.toolbarElement) {
const toolbarSize = this.toolbarElement.nativeElement.offsetHeight;
const cardHeight = 53;
const unusedHeight = 145;
@ -132,14 +151,15 @@ export class TasklistComponent implements OnInit, OnDestroy {
getTasks(): void {
this.requestInProgress = true;
if (this.currentBasket === undefined) {
if (this.currentBasket === undefined && !this.objectReference) {
this.requestInProgress = false;
this.tasks = [];
} else {
this.calculateHeightCard();
this.taskService.findTasksWithWorkbasket(this.currentBasket.workbasketId, this.sort.sortBy, this.sort.sortDirection,
this.filterBy.filterParams.name, this.filterBy.filterParams.owner, this.filterBy.filterParams.priority,
this.filterBy.filterParams.state)
this.taskService.findTasksWithWorkbasket(this.currentBasket ? this.currentBasket.workbasketId : undefined,
this.sort.sortBy, this.sort.sortDirection, this.filterBy.filterParams.name, this.filterBy.filterParams.owner,
this.filterBy.filterParams.priority, this.filterBy.filterParams.state, this.objectReference ? this.objectReference.type : undefined,
this.objectReference ? this.objectReference.value : undefined)
.subscribe(tasks => {
this.requestInProgress = false;
if (tasks._embedded) {
@ -163,5 +183,6 @@ export class TasklistComponent implements OnInit, OnDestroy {
this.workbasketChangeSubscription.unsubscribe();
this.taskAddedSubscription.unsubscribe();
this.orientationSubscription.unsubscribe();
this.objectReferenceSubscription.unsubscribe();
}
}

View File

@ -1,44 +1,45 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MasterAndDetailComponent} from '../shared/master-and-detail/master-and-detail.component';
import {TaskComponent} from './task/task.component';
import {TaskdetailsComponent} from './taskdetails/taskdetails.component';
import {TasklistComponent} from './tasklist/tasklist.component';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MasterAndDetailComponent } from '../shared/master-and-detail/master-and-detail.component';
import { TaskComponent } from './task/task.component';
import { TaskdetailsComponent } from './taskdetails/taskdetails.component';
import { TasklistComponent } from './tasklist/tasklist.component';
const routes: Routes = [
{
path: 'tasks',
component: MasterAndDetailComponent,
children: [
{
path: '',
component: TasklistComponent,
outlet: 'master'
},
{
path: 'taskdetail/:id',
component: TaskdetailsComponent,
outlet: 'detail'
},
{
path: 'task/:id',
component: TaskComponent,
outlet: 'detail'
}
]
},
{
path: '',
redirectTo: 'tasks',
pathMatch: 'full'
}
]
;
{
path: 'tasks',
component: MasterAndDetailComponent,
children: [
{
path: '',
component: TasklistComponent,
outlet: 'master'
},
{
path: 'taskdetail/:id',
component: TaskdetailsComponent,
outlet: 'detail'
},
{
path: 'task/:id',
component: TaskComponent,
outlet: 'detail'
}
]
},
{
path: '',
redirectTo: 'tasks',
pathMatch: 'full'
},
{
path: '**',
redirectTo: 'tasks'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class WorkplaceRoutingModule {
}
export class WorkplaceRoutingModule {}

View File

@ -10,5 +10,3 @@ $aquamarine: #22a39f;
$pallete-blue: #36bcee;
$pallete-green: #5fbca1;
$transparent-grey: rgba(192, 192, 192, 0.65);