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 { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core';
const MODULES = [
CommonModule,
@ -93,7 +95,9 @@ const DECLARATIONS = [
MatDividerModule,
MatListModule,
MatProgressBarModule,
MatToolbarModule
MatToolbarModule,
MatCheckboxModule,
MatRippleModule
],
providers: [
ClassificationDefinitionService,

View File

@ -14,11 +14,10 @@
color: $invalid;
}
td {
&.has-changes {
.has-changes {
border-bottom: 1px solid $brown;
}
}
.table > thead > tr > th {
max-width: 150px;
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">
<!-- 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 -->
<div class="workbasket-access-items">
<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 -->
<thead>
<tr>
<th></th>
<th class="text-align required-header">AccessID</th>
<th class="required-header">AccessID</th>
<th>Select all</th>
<th>Read</th>
<th>Open</th>
@ -44,71 +28,70 @@
<tr *ngFor="let accessItem of accessItemsGroups.controls; let index = index;" [formGroupName]="index">
<!-- REMOVE BUTTON -->
<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>
</button>
</td>
<!-- ACCESS ID -->
<td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput" class="input-group text-align text-width taskana-type-ahead"
[ngClass]="{
'has-warning': (accessItemsClone[index].accessId !== 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>
<td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput"
class="workbasket-access-items__typeahead"
[ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== 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>
</td>
<ng-template #accessIdInput>
<td class="input-group text-align text-width">
<div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !==accessItem.value.accessId), 'has-error':
!accessItem.value.accessId && formsValidatorService.formSubmitAttempt}">
<input type="text" class="form-control" formControlName="accessId" placeholder="{{accessItem.invalid?
'* Access id is required': ''}}"
[@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement>
<td>
<div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId),
'has-error': !accessItem.value.accessId && formsValidatorService.formSubmitAttempt}">
<input matInput type="text" formControlName="accessId"
placeholder="{{accessItem.invalid ? '* Access id is required': ''}}"
[@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement>
</div>
</td>
</ng-template>
<!-- SELECT ALL -->
<td>
<input id="checkbox-{{index}}-00" type="checkbox" (change)="checkAll(index, $event)">
<label for="checkbox-{{index}}-00"></label>
</td>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-00" (change)="checkAll(index, $event)" aria-label="checkAll" aria-labelledby="checkAll">
</td>
<!-- READ -->
<!-- READ -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permRead !== accessItem.value.permRead)}">
<input id="checkbox-{{index}}-0" type="checkbox" formControlName="permRead" class="regular-checkbox">
<label for="checkbox-{{index}}-0"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-0" formControlName="permRead" aria-label="permRead" aria-labelledby="permRead">
</td>
<!-- OPEN -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permOpen !== accessItem.value.permOpen)}">
<input id="checkbox-{{index}}-1" type="checkbox" formControlName="permOpen">
<label for="checkbox-{{index}}-1"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-1" formControlName="permOpen" aria-label="permOpen" aria-labelledby="permOpen">
</td>
<!-- APPEND -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permAppend !== accessItem.value.permAppend)}">
<input id="checkbox-{{index}}-2" type="checkbox" formControlName="permAppend">
<label for="checkbox-{{index}}-2"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-2" formControlName="permAppend" aria-label="permAppend" aria-labelledby="permAppend">
</td>
<!-- TRANSFER -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permTransfer !== accessItem.value.permTransfer)}">
<input id="checkbox-{{index}}-3" type="checkbox" formControlName="permTransfer">
<label for="checkbox-{{index}}-3"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-3" formControlName="permTransfer" aria-label="permTransfer" aria-labelledby="permTransfer">
</td>
<!-- DISTRIBUTE -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permDistribute !== accessItem.value.permDistribute)}">
<input id="checkbox-{{index}}-4" type="checkbox" formControlName="permDistribute">
<label for="checkbox-{{index}}-4"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-4" formControlName="permDistribute" aria-label="permDistribute" aria-labelledby="permDistribute">
</td>
<!-- CUSTOM FIELDS -->
<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)] }">
<input id="checkbox-{{index}}-{{customIndex + 5}}" type="checkbox" formControlName="permCustom{{customIndex+1}}">
<label for="checkbox-{{index}}-{{customIndex + 5}}"></label>
<input class="workbasket-access-items__permission-checkbox" type="checkbox" id="checkbox-{{index}}-{{customIndex + 5}}" formControlName="permCustom{{customIndex+1}}" aria-label="customField" aria-labelledby="customField">
</td>
</ng-container>
</tr>
@ -117,10 +100,9 @@
</form>
<!-- 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>Add new access</span>
</button>
<taskana-shared-spinner [isRunning]="requestInProgress" [positionClass]=""></taskana-shared-spinner>
</div>
</div>

View File

@ -3,17 +3,22 @@
.workbasket-access-items {
max-width: calc(100vw - 500px);
}
.workbasket-access-items__typeahead {
text-align: left;
min-width: 180px;
width: calc(100% + 20px);
}
td > input[type='checkbox'] {
margin-top: 0;
display: block;
}
.panel-body {
overflow-x: auto;
padding-top: 0;
}
.text-width {
width: 100%;
min-width: 180px;
}
.required-header {
width: 200px;
}
@ -23,22 +28,54 @@ td > input[type='checkbox'] {
}
th {
padding: 0.25rem;
position: sticky;
top: 0;
z-index: 3;
background: white;
}
td {
vertical-align: bottom !important;
&.has-changes {
border-bottom: 1px solid #f0ad4e;
}
.workbasket-access-items__table {
margin-top: 20px;
}
.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;
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 { ACTION } from '../../../shared/models/action';
import { WorkbasketAccessItems } from '../../../shared/models/workbasket-access-items';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@Component({ selector: 'taskana-shared-spinner', template: '' })
class SpinnerStub {
@ -88,7 +89,8 @@ describe('WorkbasketAccessItemsComponent', () => {
NgxsModule.forRoot([WorkbasketState, EngineConfigurationState]),
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
BrowserAnimationsModule
BrowserAnimationsModule,
MatProgressBarModule
],
declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent, SpinnerStub],
providers: [
@ -120,7 +122,6 @@ describe('WorkbasketAccessItemsComponent', () => {
workbasketAccessItems: workbasketAccessItemsMock
}
});
fixture.detectChanges();
}));
afterEach(async(() => {
@ -149,7 +150,8 @@ describe('WorkbasketAccessItemsComponent', () => {
});
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');
expect(addAccessItemButton.title).toMatch('Add new access');
@ -158,12 +160,14 @@ describe('WorkbasketAccessItemsComponent', () => {
});
it('should undo changes when undo button is clicked', () => {
fixture.detectChanges();
const clearSpy = jest.spyOn(component, 'clear');
component.clear();
expect(clearSpy).toHaveBeenCalled();
});
it('should check all permissions when check all box is checked', () => {
fixture.detectChanges();
const checkAllSpy = jest.spyOn(component, 'checkAll');
const checkAllButton = debugElement.nativeElement.querySelector('#checkbox-0-00');
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 { AccessIdDefinition } from 'app/shared/models/access-id';
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 { NotificationService } from '../../../shared/services/notifications/notification.service';
import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation';
import {
GetWorkbasketAccessItems,
OnButtonPressed,
UpdateWorkbasketAccessItems
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { WorkbasketComponent } from '../../models/workbasket-component';
import { ButtonAction } from '../../models/button-action';
@Component({
selector: 'taskana-administration-workbasket-access-items',
@ -77,11 +80,17 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
@Select(WorkbasketSelectors.workbasketAccessItems)
accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>;
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
@Select(WorkbasketSelectors.selectedComponent)
selectedComponent$: Observable<WorkbasketComponent>;
constructor(
private savingWorkbaskets: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService,
private formBuilder: FormBuilder,
private formsValidatorService: FormsValidatorService,
public formsValidatorService: FormsValidatorService,
private notificationsService: NotificationService,
private store: Store
) {}
@ -91,6 +100,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
}
ngOnInit() {
this.init();
this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount));
this.accessItemsRepresentation$.subscribe((accessItemsRepresentation) => {
if (typeof accessItemsRepresentation !== 'undefined') {
@ -100,6 +110,27 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
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() {
@ -195,6 +226,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
}
clear() {
this.store.dispatch(new OnButtonPressed(undefined));
this.formsValidatorService.formSubmitAttempt = false;
this.AccessItemsForm.reset();
this.setAccessItemsGroups(this.accessItemsResetClone);

View File

@ -6,6 +6,6 @@
</div>
<div class="navbar__logo">
<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>
</mat-toolbar>

View File

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