TSK-1411: Update Update Workbasket Access Items Component MD (#1310)

* TSK-1411: updated workbasket access items functionality to work with new action bar

* TSK-1411: updated new design, accessibility, tests for workbasket access items

* TSK-1411: remove container ripple effect which causes discrepancy between firefox and chromium

* TSK-1411: minor CSS update
This commit is contained in:
Chi Nguyen 2020-10-20 17:06:22 +02:00 committed by GitHub
parent 356e41ea27
commit a139940bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 132 additions and 73 deletions

View File

@ -46,6 +46,8 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,
@ -93,7 +95,9 @@ const DECLARATIONS = [
MatDividerModule, MatDividerModule,
MatListModule, MatListModule,
MatProgressBarModule, MatProgressBarModule,
MatToolbarModule MatToolbarModule,
MatCheckboxModule,
MatRippleModule
], ],
providers: [ providers: [
ClassificationDefinitionService, ClassificationDefinitionService,

View File

@ -14,11 +14,10 @@
color: $invalid; color: $invalid;
} }
td { .has-changes {
&.has-changes {
border-bottom: 1px solid $brown; border-bottom: 1px solid $brown;
} }
}
.table > thead > tr > th { .table > thead > tr > th {
max-width: 150px; max-width: 150px;
border-bottom: none; border-bottom: none;

View File

@ -1,32 +1,16 @@
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
<div *ngIf="workbasket" id="wb-information"> <div *ngIf="workbasket" id="wb-information">
<!-- ACTION TOOLBAR -->
<!--
<div class="panel-heading">
<div class="pull-right btn-group">
<button type="button" (click)="onSubmit()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
<span class="material-icons md-20">save</span>
</button>
<button type="button" (click)="clear()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default undo-button">
<span class="material-icons md-20 blue">undo</span>
</button>
</div>
<h4 class="panel-header">{{workbasket.name}}
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4>
</div>
-->
<!-- ACCESS ITEMS --> <!-- ACCESS ITEMS -->
<div class="workbasket-access-items"> <div class="workbasket-access-items">
<form [formGroup]="AccessItemsForm"> <form [formGroup]="AccessItemsForm">
<table formArrayName="accessItemsGroups" id="table-access-items" class="table table-striped table-center"> <table formArrayName="accessItemsGroups" id="table-access-items" class="workbasket-access-items__table table-striped">
<!-- TITLE ROW --> <!-- TITLE ROW -->
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th class="text-align required-header">AccessID</th> <th class="required-header">AccessID</th>
<th>Select all</th> <th>Select all</th>
<th>Read</th> <th>Read</th>
<th>Open</th> <th>Open</th>
@ -44,71 +28,70 @@
<tr *ngFor="let accessItem of accessItemsGroups.controls; let index = index;" [formGroupName]="index"> <tr *ngFor="let accessItem of accessItemsGroups.controls; let index = index;" [formGroupName]="index">
<!-- REMOVE BUTTON --> <!-- REMOVE BUTTON -->
<td> <td>
<button type="button" style="padding: 3px;" (click)="remove(index)" data-toggle="tooltip" title="Remove" class="btn btn-default"> <button mat-button type="button" style="padding: 3px;" (click)="remove(index)" title="Remove">
<span class="material-icons md-24 red">clear</span> <span class="material-icons md-24 red">clear</span>
</button> </button>
</td> </td>
<!-- ACCESS ID --> <!-- ACCESS ID -->
<td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput" class="input-group text-align text-width taskana-type-ahead" <td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput"
[ngClass]="{ class="workbasket-access-items__typeahead"
'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId), [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId),
'has-error': !accessItem.value.accessId }"> 'has-error': !accessItem.value.accessId }">
<taskana-shared-type-ahead formControlName="accessId" placeHolderMessage="* Access id is required" [validationValue]="toggleValidationAccessIdMap.get(index)"
[displayError]="!isFieldValid('accessItem.value.accessId', index)" (selectedItem)="accessItemSelected($event, index)" (inputField)="focusNewInput($event)"></taskana-shared-type-ahead> <taskana-shared-type-ahead formControlName="accessId" placeHolderMessage="* Access id is required"
[validationValue]="toggleValidationAccessIdMap.get(index)"
[displayError]="!isFieldValid('accessItem.value.accessId', index)"
(selectedItem)="accessItemSelected($event, index)"
(inputField)="focusNewInput($event)">
</taskana-shared-type-ahead>
</td> </td>
<ng-template #accessIdInput> <ng-template #accessIdInput>
<td class="input-group text-align text-width"> <td>
<div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !==accessItem.value.accessId), 'has-error': <div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId),
!accessItem.value.accessId && formsValidatorService.formSubmitAttempt}"> 'has-error': !accessItem.value.accessId && formsValidatorService.formSubmitAttempt}">
<input type="text" class="form-control" formControlName="accessId" placeholder="{{accessItem.invalid? <input matInput type="text" formControlName="accessId"
'* Access id is required': ''}}" placeholder="{{accessItem.invalid ? '* Access id is required': ''}}"
[@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement> [@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement>
</div> </div>
</td> </td>
</ng-template> </ng-template>
<!-- SELECT ALL --> <!-- SELECT ALL -->
<td> <td>
<input id="checkbox-{{index}}-00" type="checkbox" (change)="checkAll(index, $event)"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-00" (change)="checkAll(index, $event)" aria-label="checkAll" aria-labelledby="checkAll">
<label for="checkbox-{{index}}-00"></label> </td>
</td>
<!-- READ --> <!-- READ -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permRead !== accessItem.value.permRead)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permRead !== accessItem.value.permRead)}">
<input id="checkbox-{{index}}-0" type="checkbox" formControlName="permRead" class="regular-checkbox"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-0" formControlName="permRead" aria-label="permRead" aria-labelledby="permRead">
<label for="checkbox-{{index}}-0"></label>
</td> </td>
<!-- OPEN --> <!-- OPEN -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permOpen !== accessItem.value.permOpen)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permOpen !== accessItem.value.permOpen)}">
<input id="checkbox-{{index}}-1" type="checkbox" formControlName="permOpen"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-1" formControlName="permOpen" aria-label="permOpen" aria-labelledby="permOpen">
<label for="checkbox-{{index}}-1"></label>
</td> </td>
<!-- APPEND --> <!-- APPEND -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permAppend !== accessItem.value.permAppend)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permAppend !== accessItem.value.permAppend)}">
<input id="checkbox-{{index}}-2" type="checkbox" formControlName="permAppend"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-2" formControlName="permAppend" aria-label="permAppend" aria-labelledby="permAppend">
<label for="checkbox-{{index}}-2"></label>
</td> </td>
<!-- TRANSFER --> <!-- TRANSFER -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permTransfer !== accessItem.value.permTransfer)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permTransfer !== accessItem.value.permTransfer)}">
<input id="checkbox-{{index}}-3" type="checkbox" formControlName="permTransfer"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-3" formControlName="permTransfer" aria-label="permTransfer" aria-labelledby="permTransfer">
<label for="checkbox-{{index}}-3"></label>
</td> </td>
<!-- DISTRIBUTE --> <!-- DISTRIBUTE -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permDistribute !== accessItem.value.permDistribute)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permDistribute !== accessItem.value.permDistribute)}">
<input id="checkbox-{{index}}-4" type="checkbox" formControlName="permDistribute"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-4" formControlName="permDistribute" aria-label="permDistribute" aria-labelledby="permDistribute">
<label for="checkbox-{{index}}-4"></label>
</td> </td>
<!-- CUSTOM FIELDS --> <!-- CUSTOM FIELDS -->
<ng-container *ngFor="let customField of customFields$ | async; let customIndex = index"> <ng-container *ngFor="let customField of customFields$ | async; let customIndex = index">
<td *ngIf="customField.visible" [ngClass]="{ 'has-changes': accessItemsClone[index][getAccessItemCustomProperty(customIndex + 1)] !== accessItem.value[getAccessItemCustomProperty(customIndex+1)] }"> <td *ngIf="customField.visible" [ngClass]="{ 'has-changes': accessItemsClone[index][getAccessItemCustomProperty(customIndex + 1)] !== accessItem.value[getAccessItemCustomProperty(customIndex+1)] }">
<input id="checkbox-{{index}}-{{customIndex + 5}}" type="checkbox" formControlName="permCustom{{customIndex+1}}"> <input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-{{customIndex + 5}}" formControlName="permCustom{{customIndex+1}}" aria-label="customField" aria-labelledby="customField">
<label for="checkbox-{{index}}-{{customIndex + 5}}"></label>
</td> </td>
</ng-container> </ng-container>
</tr> </tr>
@ -117,10 +100,9 @@
</form> </form>
<!-- ADD ACCESS ITEM --> <!-- ADD ACCESS ITEM -->
<button type="button" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access" class="btn btn-default add-access-item"> <button mat-stroked-button type="button" class="workbasket-access-items__add-access" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access">
<span class="material-icons md-20 green-blue">add</span> <span class="material-icons md-20 green-blue">add</span>
<span>Add new access</span> <span>Add new access</span>
</button> </button>
<taskana-shared-spinner [isRunning]="requestInProgress" [positionClass]=""></taskana-shared-spinner>
</div> </div>
</div> </div>

View File

@ -3,17 +3,22 @@
.workbasket-access-items { .workbasket-access-items {
max-width: calc(100vw - 500px); max-width: calc(100vw - 500px);
} }
.workbasket-access-items__typeahead {
text-align: left;
min-width: 180px;
width: calc(100% + 20px);
}
td > input[type='checkbox'] { td > input[type='checkbox'] {
margin-top: 0; margin-top: 0;
display: block;
} }
.panel-body { .panel-body {
overflow-x: auto; overflow-x: auto;
padding-top: 0; padding-top: 0;
} }
.text-width {
width: 100%;
min-width: 180px;
}
.required-header { .required-header {
width: 200px; width: 200px;
} }
@ -23,22 +28,54 @@ td > input[type='checkbox'] {
} }
th { th {
padding: 0.25rem;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 3; z-index: 3;
background: white; background: white;
} }
.workbasket-access-items__table {
td { margin-top: 20px;
vertical-align: bottom !important;
&.has-changes {
border-bottom: 1px solid #f0ad4e;
}
} }
.table > thead > tr > th { .workbasket-access-items__table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
.workbasket-access-items__table td,
.table th {
padding: 0.5rem;
vertical-align: middle;
border-top: 1px solid #dee2e6;
}
.workbasket-access-items__table > thead > tr > th {
max-width: 150px; max-width: 150px;
border-bottom: none; border-bottom: none;
} }
taskana-shared-type-ahead {
top: 0; .workbasket-access-items__permission-checkbox {
display: inline-block;
height: 1rem;
line-height: 0;
margin: auto;
order: 0;
position: relative;
white-space: nowrap;
width: 1rem;
flex-shrink: 0;
cursor: pointer;
@supports (-moz-appearance: none) {
//bigger checkboxes for firefox because firefox renders differently
width: 1.25rem;
height: 1.25rem;
}
}
input[type='checkbox']:checked {
filter: hue-rotate(320deg) brightness(1);
}
.workbasket-access-items__add-access {
margin: 16px 0 16px 16px;
} }

View File

@ -33,6 +33,7 @@ import {
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ACTION } from '../../../shared/models/action'; import { ACTION } from '../../../shared/models/action';
import { WorkbasketAccessItems } from '../../../shared/models/workbasket-access-items'; import { WorkbasketAccessItems } from '../../../shared/models/workbasket-access-items';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@Component({ selector: 'taskana-shared-spinner', template: '' }) @Component({ selector: 'taskana-shared-spinner', template: '' })
class SpinnerStub { class SpinnerStub {
@ -88,7 +89,8 @@ describe('WorkbasketAccessItemsComponent', () => {
NgxsModule.forRoot([WorkbasketState, EngineConfigurationState]), NgxsModule.forRoot([WorkbasketState, EngineConfigurationState]),
HttpClientTestingModule, HttpClientTestingModule,
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
BrowserAnimationsModule BrowserAnimationsModule,
MatProgressBarModule
], ],
declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent, SpinnerStub], declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent, SpinnerStub],
providers: [ providers: [
@ -120,7 +122,6 @@ describe('WorkbasketAccessItemsComponent', () => {
workbasketAccessItems: workbasketAccessItemsMock workbasketAccessItems: workbasketAccessItemsMock
} }
}); });
fixture.detectChanges();
})); }));
afterEach(async(() => { afterEach(async(() => {
@ -149,7 +150,8 @@ describe('WorkbasketAccessItemsComponent', () => {
}); });
it('should add accessItems when add access item button is clicked', () => { it('should add accessItems when add access item button is clicked', () => {
const addAccessItemButton = debugElement.nativeElement.querySelector('button.add-access-item'); fixture.detectChanges();
const addAccessItemButton = debugElement.nativeElement.querySelector('button.workbasket-access-items__add-access');
const clearSpy = jest.spyOn(component, 'addAccessItem'); const clearSpy = jest.spyOn(component, 'addAccessItem');
expect(addAccessItemButton.title).toMatch('Add new access'); expect(addAccessItemButton.title).toMatch('Add new access');
@ -158,12 +160,14 @@ describe('WorkbasketAccessItemsComponent', () => {
}); });
it('should undo changes when undo button is clicked', () => { it('should undo changes when undo button is clicked', () => {
fixture.detectChanges();
const clearSpy = jest.spyOn(component, 'clear'); const clearSpy = jest.spyOn(component, 'clear');
component.clear(); component.clear();
expect(clearSpy).toHaveBeenCalled(); expect(clearSpy).toHaveBeenCalled();
}); });
it('should check all permissions when check all box is checked', () => { it('should check all permissions when check all box is checked', () => {
fixture.detectChanges();
const checkAllSpy = jest.spyOn(component, 'checkAll'); const checkAllSpy = jest.spyOn(component, 'checkAll');
const checkAllButton = debugElement.nativeElement.querySelector('#checkbox-0-00'); const checkAllButton = debugElement.nativeElement.querySelector('#checkbox-0-00');
expect(checkAllButton).toBeTruthy(); expect(checkAllButton).toBeTruthy();

View File

@ -25,15 +25,18 @@ import { highlight } from 'app/shared/animations/validation.animation';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { AccessIdDefinition } from 'app/shared/models/access-id'; import { AccessIdDefinition } from 'app/shared/models/access-id';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { takeUntil } from 'rxjs/operators'; import { filter, take, takeUntil } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation'; import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation';
import { import {
GetWorkbasketAccessItems, GetWorkbasketAccessItems,
OnButtonPressed,
UpdateWorkbasketAccessItems UpdateWorkbasketAccessItems
} from '../../../shared/store/workbasket-store/workbasket.actions'; } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors'; import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { WorkbasketComponent } from '../../models/workbasket-component';
import { ButtonAction } from '../../models/button-action';
@Component({ @Component({
selector: 'taskana-administration-workbasket-access-items', selector: 'taskana-administration-workbasket-access-items',
@ -77,11 +80,17 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
@Select(WorkbasketSelectors.workbasketAccessItems) @Select(WorkbasketSelectors.workbasketAccessItems)
accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>; accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>;
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
@Select(WorkbasketSelectors.selectedComponent)
selectedComponent$: Observable<WorkbasketComponent>;
constructor( constructor(
private savingWorkbaskets: SavingWorkbasketService, private savingWorkbaskets: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService, private requestInProgressService: RequestInProgressService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private formsValidatorService: FormsValidatorService, public formsValidatorService: FormsValidatorService,
private notificationsService: NotificationService, private notificationsService: NotificationService,
private store: Store private store: Store
) {} ) {}
@ -91,6 +100,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
} }
ngOnInit() { ngOnInit() {
this.init();
this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount)); this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount));
this.accessItemsRepresentation$.subscribe((accessItemsRepresentation) => { this.accessItemsRepresentation$.subscribe((accessItemsRepresentation) => {
if (typeof accessItemsRepresentation !== 'undefined') { if (typeof accessItemsRepresentation !== 'undefined') {
@ -100,6 +110,27 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
this.accessItemsResetClone = this.cloneAccessItems(accessItemsRepresentation.accessItems); this.accessItemsResetClone = this.cloneAccessItems(accessItemsRepresentation.accessItems);
} }
}); });
this.buttonAction$
.pipe(takeUntil(this.destroy$))
.pipe(filter((buttonAction) => typeof buttonAction !== 'undefined'))
.subscribe((button) => {
this.selectedComponent$
.pipe(take(1))
.pipe(filter((component) => component === WorkbasketComponent.ACCESS_ITEMS))
.subscribe((component) => {
switch (button) {
case ButtonAction.SAVE:
this.onSubmit();
break;
case ButtonAction.UNDO:
this.clear();
break;
default:
break;
}
});
});
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -195,6 +226,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
} }
clear() { clear() {
this.store.dispatch(new OnButtonPressed(undefined));
this.formsValidatorService.formSubmitAttempt = false; this.formsValidatorService.formSubmitAttempt = false;
this.AccessItemsForm.reset(); this.AccessItemsForm.reset();
this.setAccessItemsGroups(this.accessItemsResetClone); this.setAccessItemsGroups(this.accessItemsResetClone);

View File

@ -6,6 +6,6 @@
</div> </div>
<div class="navbar__logo"> <div class="navbar__logo">
<svg-icon class="navbar__logo-icon" src="./assets/icons/logo-copy.svg"></svg-icon> <svg-icon class="navbar__logo-icon" src="./assets/icons/logo-copy.svg"></svg-icon>
<div class="navbar__title">/ {{title}}</div> <div class="navbar__title">{{ title }}</div>
</div> </div>
</mat-toolbar> </mat-toolbar>

View File

@ -262,6 +262,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx: StateContext<WorkbasketStateModel>, ctx: StateContext<WorkbasketStateModel>,
action: UpdateWorkbasketAccessItems action: UpdateWorkbasketAccessItems
): Observable<any> { ): Observable<any> {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.updateWorkBasketAccessItem(action.url, action.workbasketAccessItems).pipe( return this.workbasketService.updateWorkBasketAccessItem(action.url, action.workbasketAccessItems).pipe(
take(1), take(1),
tap( tap(