From 6a8311a32be986c96858e18bb6fa4b1f78ce21b0 Mon Sep 17 00:00:00 2001 From: Chi Nguyen <6671583+cnguyen-de@users.noreply.github.com> Date: Tue, 6 Oct 2020 09:58:30 +0200 Subject: [PATCH] TSK-1398: New material design for classification component (#1287) * TSK-1394: Remove bootstrap 3 * TSK-1394: Temporarily remove cypress tests * TSK-1398: fix classification component display * TSK-1399: fix workbasket display * TSK-1398: Fixed category selection in classification component * TSK-1398: Fixed category selection in classification component TSK-1398: * TSK-1398: Added selectCategory method * TSK-1398: Refactored Classification-Types-Selector * TSK-1398: Replaced category dropdown in classification details by Select * TSK-1398: Removed color of category icons * TSK-1398: added new administration overview navbar * TSK-1398: Removed bootstrap from action toolbar in classification-list * TSK-1398: Removed bootstrap from import-export component * TSK-1398: update visual in action bar in classification list and classification tree * TSK-1398: Removed bootstrap classes from classification-list * TSK-1398: update design * TSK-1398: update classification list design * TSK-1398: Refactored action-toolbar in classification-details * TSK-1398: rework administration routing configuration, now nested in mat-tabs * TSK-1398: administration tabs displays correctly when refreshing/first time open * TSK-1398: fixed overall CSS errors * TSK-1398: design update, added tooltips to buttons * TSK-1398: minor visual changes * TSK-1398: Refactored classification-details * TSK-1398: Fixed category selector in classification-details * TSK-1398: Modified category-icon css * TSK-1398: update classification details actionbar design * TSK-1398: minor design update, correctly highlight tab when details is showing * TSK-1398: Fixed tests in classification-details * TSK-1398: fixed "valid in domain" text not showing * TSK-1398: add domain selector in administration overview tabs * TSK-1398: Fixed tests in classification-list * TSK-1398: added unit tests for admin overview * TSK-1398: Update design of action bar in classification list * TSK-1398: Fixed tests * TSK-1398: minor CSS, tooltips update * TSK-1398: fix broken test in classification list Co-authored-by: Sofie Hofmann <29145005+sofie29@users.noreply.github.com> --- .../administration-routing.module.ts | 92 ++--- .../administration/administration.module.ts | 27 +- .../access-items-management.component.html | 2 +- .../access-items-management.component.scss | 4 + .../administration-overview.component.html | 21 ++ .../administration-overview.component.scss | 27 ++ .../administration-overview.component.spec.ts | 61 +++ .../administration-overview.component.ts | 52 +++ .../classification-details.component.html | 356 +++++++++--------- .../classification-details.component.scss | 112 +++++- .../classification-details.component.spec.ts | 126 +++++-- .../classification-details.component.ts | 15 +- .../classification-list.component.html | 122 +++--- .../classification-list.component.scss | 69 ++-- .../classification-list.component.spec.ts | 66 +++- .../classification-list.component.ts | 24 +- .../classification-overview.component.html | 12 +- .../classification-overview.component.scss | 10 + ...assification-types-selector.component.html | 25 +- ...assification-types-selector.component.scss | 3 + ...ification-types-selector.component.spec.ts | 45 ++- .../import-export.component.html | 36 +- .../import-export.component.scss | 6 + .../import-export/import-export.component.ts | 12 +- .../components/tree/tree.component.html | 2 +- .../components/tree/tree.component.scss | 6 +- .../workbasket-details.component.html | 2 +- .../workbasket-list.component.html | 2 +- .../workbasket-list.component.scss | 3 + .../workbasket-overview.component.html | 6 +- .../workbasket-overview.component.scss | 5 + .../workbasket-overview.component.ts | 2 +- web/src/app/app.component.html | 5 +- .../master-and-detail.component.html | 2 +- .../master-and-detail.component.scss | 4 + .../components/nav-bar/nav-bar.component.scss | 1 + .../classification-categories.service.spec.ts | 4 +- .../classification-categories.service.ts | 6 +- web/src/app/shared/shared.module.ts | 4 +- .../classification.selectors.ts | 5 - web/src/theme/_site.scss | 12 +- 41 files changed, 932 insertions(+), 464 deletions(-) create mode 100644 web/src/app/administration/components/administration-overview/administration-overview.component.html create mode 100644 web/src/app/administration/components/administration-overview/administration-overview.component.scss create mode 100644 web/src/app/administration/components/administration-overview/administration-overview.component.spec.ts create mode 100644 web/src/app/administration/components/administration-overview/administration-overview.component.ts diff --git a/web/src/app/administration/administration-routing.module.ts b/web/src/app/administration/administration-routing.module.ts index 47644ef12..fca9236f6 100644 --- a/web/src/app/administration/administration-routing.module.ts +++ b/web/src/app/administration/administration-routing.module.ts @@ -5,69 +5,77 @@ import { DomainGuard } from 'app/shared/guards/domain.guard'; import { AccessItemsManagementComponent } from './components/access-items-management/access-items-management.component'; import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component'; import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component'; +import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component'; const routes: Routes = [ { - path: 'workbaskets', - component: WorkbasketOverviewComponent, + path: '', + component: AdministrationOverviewComponent, canActivate: [DomainGuard], children: [ { - path: '', + path: 'workbaskets', component: WorkbasketOverviewComponent, - outlet: 'master' + canActivate: [DomainGuard], + children: [ + { + path: '', + component: WorkbasketOverviewComponent, + outlet: 'master' + }, + { + path: ':id', + component: WorkbasketOverviewComponent, + outlet: 'detail' + }, + { + path: '**', + redirectTo: '' + } + ] }, { - path: ':id', - component: WorkbasketOverviewComponent, - outlet: 'detail' - }, - { - path: '**', - redirectTo: '' - } - ] - }, - { - path: 'classifications', - component: ClassificationOverviewComponent, - canActivate: [DomainGuard], - children: [ - { - path: '', + path: 'classifications', component: ClassificationOverviewComponent, - outlet: 'master' + canActivate: [DomainGuard], + children: [ + { + path: '', + component: ClassificationOverviewComponent, + outlet: 'master' + }, + { + path: ':id', + component: ClassificationOverviewComponent, + outlet: 'detail' + }, + { + path: '**', + redirectTo: '' + } + ] }, { - path: ':id', - component: ClassificationOverviewComponent, - outlet: 'detail' - }, - { - path: '**', - redirectTo: '' - } - ] - }, - { - path: 'access-items-management', - component: AccessItemsManagementComponent, - canActivate: [DomainGuard], - children: [ - { - path: '**', - redirectTo: '' + path: 'access-items-management', + component: AccessItemsManagementComponent, + canActivate: [DomainGuard], + children: [ + { + path: '**', + redirectTo: '' + } + ] } ] }, { path: '', - redirectTo: 'workbaskets', + redirectTo: '', pathMatch: 'full' }, { path: '**', - redirectTo: 'workbaskets' + redirectTo: '' } ]; diff --git a/web/src/app/administration/administration.module.ts b/web/src/app/administration/administration.module.ts index 1826f2d02..990f72de1 100644 --- a/web/src/app/administration/administration.module.ts +++ b/web/src/app/administration/administration.module.ts @@ -34,6 +34,16 @@ import { WorkbasketDefinitionService } from './services/workbasket-definition.se import { ImportExportService } from './services/import-export.service'; import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component'; import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTabsModule } from '@angular/material/tabs'; +import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component'; +import { MatInputModule } from '@angular/material/input'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDividerModule } from '@angular/material/divider'; const MODULES = [ CommonModule, @@ -61,12 +71,25 @@ const DECLARATIONS = [ ClassificationTypesSelectorComponent, ClassificationDetailsComponent, ImportExportComponent, - AccessItemsManagementComponent + AccessItemsManagementComponent, + AdministrationOverviewComponent ]; @NgModule({ declarations: DECLARATIONS, - imports: [MODULES, MatRadioModule], + imports: [ + MODULES, + MatRadioModule, + MatFormFieldModule, + MatSelectModule, + MatMenuModule, + MatIconModule, + MatButtonModule, + MatTabsModule, + MatInputModule, + MatTooltipModule, + MatDividerModule + ], providers: [ ClassificationDefinitionService, WorkbasketDefinitionService, diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.html b/web/src/app/administration/components/access-items-management/access-items-management.component.html index a2acc8eb4..534cda507 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.html +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.html @@ -1,4 +1,4 @@ -
+

Access items management

diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.scss b/web/src/app/administration/components/access-items-management/access-items-management.component.scss index 199fce722..babc13209 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.scss +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.scss @@ -1,5 +1,9 @@ @import '../../../../theme/colors'; +.access-items-management { + min-width: 100%; +} + .margin { margin-top: 10px; margin-bottom: 20px; diff --git a/web/src/app/administration/components/administration-overview/administration-overview.component.html b/web/src/app/administration/components/administration-overview/administration-overview.component.html new file mode 100644 index 000000000..adbc63c1b --- /dev/null +++ b/web/src/app/administration/components/administration-overview/administration-overview.component.html @@ -0,0 +1,21 @@ +
+ +
+ + + + {{domain? domain: 'MASTER DOMAIN'}} + + + +
+ +
diff --git a/web/src/app/administration/components/administration-overview/administration-overview.component.scss b/web/src/app/administration/components/administration-overview/administration-overview.component.scss new file mode 100644 index 000000000..1b1fd38a4 --- /dev/null +++ b/web/src/app/administration/components/administration-overview/administration-overview.component.scss @@ -0,0 +1,27 @@ +@import 'src/theme/_colors.scss'; + +.administration-overview__navbar { + position: relative; +} + +.administration-overview__navbar { + background-color: $light-grey; +} + +.administration-overview__domain { + max-height: 48px; + position: absolute; + top: 46px; + right: 24px; + z-index: 11; +} + +.mat-tab-link { + color: #1a202c !important; +} + +.mat-tab-label-active { + font-weight: bolder !important; + color: unset; + opacity: 1; +} diff --git a/web/src/app/administration/components/administration-overview/administration-overview.component.spec.ts b/web/src/app/administration/components/administration-overview/administration-overview.component.spec.ts new file mode 100644 index 000000000..160066c2e --- /dev/null +++ b/web/src/app/administration/components/administration-overview/administration-overview.component.spec.ts @@ -0,0 +1,61 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { AdministrationOverviewComponent } from './administration-overview.component'; +import { DebugElement } from '@angular/core'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTabsModule } from '@angular/material/tabs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DomainService } from '../../../shared/services/domain/domain.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +const domainServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + getDomains: jest.fn().mockReturnValue(of(['domain a', 'domain b'])), + getSelectedDomain: jest.fn().mockReturnValue(of('domain a')), + switchDomain: jest.fn() + }) +); + +describe('AdministrationOverviewComponent', () => { + let component: AdministrationOverviewComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatSelectModule, + MatTabsModule, + RouterTestingModule.withRoutes([]), + HttpClientTestingModule, + BrowserAnimationsModule + ], + declarations: [AdministrationOverviewComponent], + providers: [{ provide: DomainService, useClass: domainServiceSpy }] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdministrationOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should render 3 tabs in navbar', () => { + const navbar = fixture.debugElement.nativeElement.getElementsByClassName('administration-overview__navbar-links'); + expect(navbar).toHaveLength(3); + }); + + it('should display current domain', () => { + const domainElem = fixture.debugElement.nativeElement.querySelector('.administration-overview__domain'); + expect(domainElem).toBeTruthy(); + + fixture.detectChanges(); + expect(domainElem.textContent).toMatch('domain a'); + }); +}); diff --git a/web/src/app/administration/components/administration-overview/administration-overview.component.ts b/web/src/app/administration/components/administration-overview/administration-overview.component.ts new file mode 100644 index 000000000..1330a8124 --- /dev/null +++ b/web/src/app/administration/components/administration-overview/administration-overview.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { DomainService } from '../../../shared/services/domain/domain.service'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'taskana-administration-overview', + templateUrl: './administration-overview.component.html', + styleUrls: ['./administration-overview.component.scss'] +}) +export class AdministrationOverviewComponent implements OnInit { + @Input() selectedTab = ''; + domains: Array = []; + selectedDomain: string; + + destroy$ = new Subject(); + + constructor(private router: Router, private domainService: DomainService) {} + + ngOnInit() { + this.domainService + .getDomains() + .pipe(takeUntil(this.destroy$)) + .subscribe((domains) => { + this.domains = domains; + }); + + this.domainService + .getSelectedDomain() + .pipe(takeUntil(this.destroy$)) + .subscribe((domain) => { + this.selectedDomain = domain; + }); + + const urlPaths = this.router.url.split('/'); + if (this.router.url.includes('detail')) { + this.selectedTab = urlPaths[urlPaths.length - 2]; + } else { + this.selectedTab = urlPaths[urlPaths.length - 1]; + } + } + + switchDomain(domain) { + this.domainService.switchDomain(domain); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/web/src/app/administration/components/classification-details/classification-details.component.html b/web/src/app/administration/components/classification-details/classification-details.component.html index 3b52e15c5..e30cdfda1 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.html +++ b/web/src/app/administration/components/classification-details/classification-details.component.html @@ -1,189 +1,209 @@ -
+
-
-
+
- -
-
- - - - -
-

{{classification.name}}  [{{classification.type}}] - {{badgeMessage$ | async}} -

+ +
+

{{classification.name}}  [{{classification.type}}] + {{badgeMessage$ | async}} +

+ +
+ + + + + + + + + + +
- -
- -
-
+
- - -
- - -
{{lengthError}}
- - -
+ +
+ +
- -
- - -
{{lengthError}}
- - -
+
General
+ - - + + Key + + + +
{{lengthError}}
+ - -
-
- - - - -
-
- - -
-
-
-
+ + + Name + + + +
{{lengthError}}
+ - -
- - -
{{lengthError}}
-
- -
- - -
{{lengthError}}
-
+ +
+ + Service Level + + + +
{{lengthError}}
+ +
+ + + Priority + + + + + + - -
- - -
{{lengthError}}
-
+ + +
+ + + Domain + + + + + + + + Category + + + + + {{this.classification.category}} + + + + {{category}} + + + +
+ + + + + Application entry point + + + +
{{lengthError}}
+ + + + Description + + + +
{{lengthError}}
+ + -
-
-
- - -
{{lengthError}}
-
+
Custom Fields
+ + + +
+
+ + {{customField.field}} + + + +
{{lengthError}}
- -
+ +
+
diff --git a/web/src/app/administration/components/classification-details/classification-details.component.scss b/web/src/app/administration/components/classification-details/classification-details.component.scss index 3e435f07c..be6a2fd56 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.scss +++ b/web/src/app/administration/components/classification-details/classification-details.component.scss @@ -1,22 +1,114 @@ @import 'src/theme/_colors.scss'; -.custom-field-row { - display: flex; - flex-wrap: wrap; - flex-direction: column; - height: 40vh; +.classification-details { width: 100%; - margin: 0; + height: calc(100vh - 100px); + overflow-y: auto; } -.custom-field-wrapper { - height: 70px; - padding: 0 15px; +.classification-details__wrapper { + position: relative; } -.dropdown-menu > li { + + +/* ACTION TOOLBAR */ +.classification-details__headline { + padding-top: 0.5rem; +} +.classification-details__action-toolbar { + width: calc(100% - 450px); + position: fixed; + padding: 12px 32px 12px 24px; + background-color: #fff; + display: flex; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + justify-content: space-between; + flex-wrap: wrap; + z-index: 10; +} + +.action-toolbar__button { + margin-top: 0.25rem; + margin-right: 6px; + background-color: #fff; +} + +.action-toolbar__dropdown { + border-color: $transparent-grey; + border-bottom-style: solid; + border-width: 1px; +} + +.action-toolbar__save-button { + background-color: $aquamarine; + color: white; +} + +.button__green-blue { + color: $aquamarine; +} + +.button__red { + color: $invalid; +} + + +/* DETAILED FIELDS */ + +.classification__detailed-fields { + padding: 15px; + display: flex; + flex-direction: column; +} + +.classification-details__subheading { + font-weight: bold; + padding-left: 15px; + margin-bottom: 0; +} + +.classification-details__horizontal-line { + margin: 5px 5px 25px 5px; + border-top-color: #555; + border-top-width: 1.35px; +} + +.classification-details__domain-and-category { + position: relative; + display: flex; + justify-content: space-between; + padding-bottom: 16px; +} + +.domain-and-category__domain-checkbox { + position: absolute; + top: 64px; + left: 12px; + font-size: 14px; + color: #555; +} +.domain-and-category__domain-checkbox-icon { cursor: pointer; + margin-top: 2px; + font-size: 20px; +} +.domain-and-category__category-icon { + fill: #555; + margin-right: 5px; + top: -2px; +} + +.classification-details__service-and-priority { + display: flex; + justify-content: space-between; +} + +.classification-details__mat-form-field { + width: 70%; + margin-right: 10px; } input:invalid.dirty { border-color: $invalid; } + diff --git a/web/src/app/administration/components/classification-details/classification-details.component.spec.ts b/web/src/app/administration/components/classification-details/classification-details.component.spec.ts index 2713daf7f..98b544aa3 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.spec.ts +++ b/web/src/app/administration/components/classification-details/classification-details.component.spec.ts @@ -4,7 +4,7 @@ import { Observable, of } from 'rxjs'; import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service'; import { DomainService } from '../../../shared/services/domain/domain.service'; import { ImportExportService } from '../../services/import-export.service'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store'; import { ClassificationState } from '../../../shared/store/classification-store/classification.state'; import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state'; @@ -13,9 +13,6 @@ import { ClassificationDetailsComponent } from './classification-details.compone import { FormsModule } from '@angular/forms'; import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service'; import { FormsValidatorService } from '../../../shared/services/forms-validator/forms-validator.service'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatDialogModule } from '@angular/material/dialog'; -import { NumberPickerComponent } from '../../../shared/components/number-picker/number-picker.component'; import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { CopyClassification, @@ -24,6 +21,15 @@ import { SaveCreatedClassification, SaveModifiedClassification } from '../../../shared/store/classification-store/classification.actions'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; @Component({ selector: 'taskana-shared-spinner', template: '' }) class SpinnerStub { @@ -116,15 +122,20 @@ describe('ClassificationDetailsComponent', () => { imports: [ NgxsModule.forRoot([ClassificationState, EngineConfigurationState]), FormsModule, - MatSnackBarModule, - MatDialogModule + MatIconModule, + MatDividerModule, + MatFormFieldModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatMenuModule, + BrowserAnimationsModule ], declarations: [ ClassificationDetailsComponent, SpinnerStub, InputStub, FieldErrorDisplayStub, - NumberPickerComponent, SvgIconStub, TextareaStub ], @@ -247,7 +258,7 @@ describe('ClassificationDetailsComponent', () => { component.spinnerIsRunning = true; component.classification = {}; fixture.detectChanges(); - expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeFalsy(); + expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeFalsy(); expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy(); }); @@ -255,12 +266,12 @@ describe('ClassificationDetailsComponent', () => { component.spinnerIsRunning = false; component.classification = null; fixture.detectChanges(); - expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeFalsy(); + expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeFalsy(); expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy(); }); it('should show details when classification exists and spinner is not running', () => { - expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeTruthy(); + expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeTruthy(); expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeTruthy(); }); @@ -269,7 +280,7 @@ describe('ClassificationDetailsComponent', () => { component.classification = { name: 'Recommendation', type: 'DOCUMENT' }; component.isCreatingNewClassification = true; fixture.detectChanges(); - const headline = debugElement.nativeElement.querySelector('.classification__headline'); + const headline = debugElement.nativeElement.querySelector('.classification-details__headline'); expect(headline).toBeTruthy(); expect(headline.textContent).toContain('Recommendation'); expect(headline.textContent).toContain('DOCUMENT'); @@ -278,19 +289,22 @@ describe('ClassificationDetailsComponent', () => { expect(badgeMessage.textContent.trim()).toBe('Creating new classification'); }); - it('should call onSubmit() when button is clicked', () => { - const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[0]; + it('should call onSubmit() when button is clicked', async () => { + const button = debugElement.nativeElement.querySelector('.action-toolbar__save-button'); expect(button).toBeTruthy(); - expect(button.title).toBe('Save'); + expect(button.textContent).toContain('Save'); + expect(button.textContent).toContain('save'); component.onSubmit = jest.fn().mockImplementation(); button.click(); expect(component.onSubmit).toHaveBeenCalled(); }); it('should restore selected classification when button is clicked', async () => { - const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[1]; + const button = debugElement.nativeElement.querySelector('.classification-details__action-toolbar').children[1] + .children[1]; expect(button).toBeTruthy(); - expect(button.title).toBe('Restore Previous Version'); + expect(button.textContent).toContain('Undo Changes'); + expect(button.textContent).toContain('restore'); let isActionDispatched = false; actions$.pipe(ofActionDispatched(RestoreSelectedClassification)).subscribe(() => (isActionDispatched = true)); @@ -298,22 +312,39 @@ describe('ClassificationDetailsComponent', () => { expect(isActionDispatched).toBe(true); }); - it('should call onCopy() when button is clicked', () => { - const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[2]; + it('should display button to show more actions', () => { + const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons'); expect(button).toBeTruthy(); - expect(button.title).toBe('Copy'); - component.onCopy = jest.fn().mockImplementation(); button.click(); + fixture.detectChanges(); + const buttonsInDropdown = debugElement.queryAll(By.css('.action-toolbar__dropdown')); + expect(buttonsInDropdown.length).toEqual(3); + }); + + it('should call onCopy() when button is clicked', () => { + const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons'); + expect(button).toBeTruthy(); + button.click(); + fixture.detectChanges(); + const copyButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[0]; + expect(copyButton.nativeElement.textContent).toContain('content_copy'); + expect(copyButton.nativeElement.textContent).toContain('Copy'); + component.onCopy = jest.fn().mockImplementation(); + copyButton.nativeElement.click(); expect(component.onCopy).toHaveBeenCalled(); }); it('should call onRemoveClassification() when button is clicked', () => { - const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[3]; + const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons'); expect(button).toBeTruthy(); - expect(button.title).toBe('Delete'); + button.click(); + fixture.detectChanges(); + const deleteButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[1]; + expect(deleteButton.nativeElement.textContent).toContain('delete'); + expect(deleteButton.nativeElement.textContent).toContain('Delete'); const onRemoveClassificationSpy = jest.spyOn(component, 'onRemoveClassification'); - button.click(); + deleteButton.nativeElement.click(); expect(onRemoveClassificationSpy).toHaveBeenCalled(); onRemoveClassificationSpy.mockReset(); @@ -323,27 +354,54 @@ describe('ClassificationDetailsComponent', () => { expect(showDialogSpy).toHaveBeenCalled(); }); + it('should call onClose() when button is clicked', () => { + const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons'); + expect(button).toBeTruthy(); + button.click(); + fixture.detectChanges(); + const closeButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[2]; + expect(closeButton.nativeElement.textContent).toContain('close'); + expect(closeButton.nativeElement.textContent).toContain('close'); + component.onCloseClassification = jest.fn().mockImplementation(); + closeButton.nativeElement.click(); + expect(component.onCloseClassification).toHaveBeenCalled(); + }); + /* DETAILED FIELDS */ it('should display field-error-display component', () => { expect(debugElement.nativeElement.querySelector('taskana-shared-field-error-display')).toBeTruthy(); }); - it('should display number-picker component', () => { - expect(debugElement.nativeElement.querySelector('taskana-shared-number-picker')).toBeTruthy(); + it('should display form field for key', () => { + expect(debugElement.nativeElement.querySelector('#classification-key')).toBeTruthy(); }); - it('should select category when button is clicked', () => { - component.classification.category = 'A'; - component.getAvailableCategories = jest.fn().mockImplementation((type) => of(['B', 'C'])); - fixture.detectChanges(); - const button = debugElement.nativeElement.querySelector('.detailed-fields__categories'); - expect(button).toBeTruthy(); - button.click(); - expect(component.classification.category).toBe('B'); + it('should display form field for name', () => { + expect(debugElement.nativeElement.querySelector('#classification-name')).toBeTruthy(); + }); + + it('should display form field for service level', () => { + expect(debugElement.nativeElement.querySelector('#classification-service-level')).toBeTruthy(); + }); + + it('should display form field for priority', () => { + expect(debugElement.nativeElement.querySelector('#classification-priority')).toBeTruthy(); + }); + + it('should display form field for domain', () => { + expect(debugElement.nativeElement.querySelector('#classification-domain')).toBeTruthy(); + }); + + it('should display form field for application entry point', () => { + expect(debugElement.nativeElement.querySelector('#classification-application-entry-point')).toBeTruthy(); + }); + + it('should display form field for description', () => { + expect(debugElement.nativeElement.querySelector('#classification-description')).toBeTruthy(); }); it('should change isValidInDomain when button is clicked', () => { - const button = debugElement.nativeElement.querySelector('.detailed-fields__domain').children[2]; + const button = debugElement.nativeElement.querySelector('.domain-and-category__domain-checkbox-icon').parentNode; expect(button).toBeTruthy(); component.classification.isValidInDomain = false; button.click(); diff --git a/web/src/app/administration/components/classification-details/classification-details.component.ts b/web/src/app/administration/components/classification-details/classification-details.component.ts index 4829ffcae..7c75bac35 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.ts +++ b/web/src/app/administration/components/classification-details/classification-details.component.ts @@ -28,7 +28,8 @@ import { RestoreSelectedClassification, SaveModifiedClassification, SelectClassification, - CopyClassification + CopyClassification, + DeselectClassification } from '../../../shared/store/classification-store/classification.actions'; @Component({ @@ -43,7 +44,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy { @Select(ClassificationSelectors.selectCategories) categories$: Observable; @Select(EngineConfigurationSelectors.selectCategoryIcons) categoryIcons$: Observable; @Select(ClassificationSelectors.selectedClassificationType) selectedClassificationType$: Observable; - @Select(ClassificationSelectors.selectClassificationTypesObject) classificationTypes$: Observable; @Select(ClassificationSelectors.selectedClassification) selectedClassification$: Observable; @Select(ClassificationSelectors.getBadgeMessage) badgeMessage$: Observable; @@ -127,8 +127,8 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy { } } - selectCategory(category: string) { - this.classification.category = category; + onCloseClassification() { + this.store.dispatch(new DeselectClassification()); } getCategoryIcon(category: string): Observable { @@ -157,13 +157,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy { return `custom${customNumber}`; } - getAvailableCategories(type: string): Observable { - return this.classificationTypes$.pipe( - take(1), - map((classTypes) => classTypes[type]) - ); - } - async onSave() { this.requestInProgressService.setRequestInProgress(true); if (typeof this.classification.classificationId === 'undefined') { diff --git a/web/src/app/administration/components/classification-list/classification-list.component.html b/web/src/app/administration/components/classification-list/classification-list.component.html index 5d864668d..ce4a96596 100644 --- a/web/src/app/administration/components/classification-list/classification-list.component.html +++ b/web/src/app/administration/components/classification-list/classification-list.component.html @@ -1,63 +1,73 @@ -
+
- -
  • -
    -
    + +
    +
    + - + + - - -
    -
    - - -
    -
    -
  • + +
    - -
    - - -
    -
    - -
    + +
    +
    + -
    + + + + +
    - - - - -
    -

    There are no classifications

    - -
    -
    +
    + + Filter classification + + +
    + + + +
    + + + + + + + +
    +

    There are no classifications

    +
    +
    diff --git a/web/src/app/administration/components/classification-list/classification-list.component.scss b/web/src/app/administration/components/classification-list/classification-list.component.scss index d5850021b..5da580153 100644 --- a/web/src/app/administration/components/classification-list/classification-list.component.scss +++ b/web/src/app/administration/components/classification-list/classification-list.component.scss @@ -1,38 +1,53 @@ -.classification-list-full-height { +@import 'src/theme/_colors.scss'; + +.classification-list { height: calc(100vh - 55px); + width: 450px; } - -.list-group-item { - padding: 5px 0px 2px 1px; +.classification-list__action-toolbar { + padding: 0 16px; + min-height: 68px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} +.classification-list__action-buttons { + display: flex; border: none; + margin-bottom: 0; + padding: 16px 4px 8px 4px; +} +.action-toolbar__add-button { + background-color: $aquamarine; + color: white; } -.tab-align { - margin-bottom: 0px; - border-bottom: 1px dotted #ddd; - padding: 8px 12px 8px 4px; - & > div { - margin: 6px 0px; - } +.classification-list__import-export { + display: flex; +} +.classification-list__filter { + display: flex; } -input.filter-input { - border: solid 1px grey; - margin: 10px 2px; - height: 32px; +.classification-list__category-filter { + padding-top: 7px; +} +.category-filter__icons { + height: 33px; + width: 16px; +} +.category-filter__categories { + fill: #555; + margin: 0; + top: -2px; +} + +.filter__input { width: 100%; - padding-left: 10px; + margin-right: 12px; } - -.category-filter { - margin: 7px 2px; +.filter__input-field { + width: 100% !important; } - -.dropdown-menu-classification { - margin-left: 15px; - min-width: 0px; -} - -div.category-filter svg-icon { - position: initial; +.classification-list__no-items { + text-align: center; + padding-top: 150px; } diff --git a/web/src/app/administration/components/classification-list/classification-list.component.spec.ts b/web/src/app/administration/components/classification-list/classification-list.component.spec.ts index ce666fdb2..e55b90125 100644 --- a/web/src/app/administration/components/classification-list/classification-list.component.spec.ts +++ b/web/src/app/administration/components/classification-list/classification-list.component.spec.ts @@ -12,6 +12,12 @@ import { ImportExportService } from '../../services/import-export.service'; import { Observable, of } from 'rxjs'; import { CreateClassification } from '../../../shared/store/classification-store/classification.actions'; import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatInputModule } from '@angular/material/input'; +import { By } from '@angular/platform-browser'; @Component({ selector: 'taskana-administration-import-export', template: '' }) class ImportExportStub { @@ -76,7 +82,14 @@ describe('ClassificationListComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([ClassificationState, EngineConfigurationState])], + imports: [ + NgxsModule.forRoot([ClassificationState, EngineConfigurationState]), + MatIconModule, + MatMenuModule, + MatFormFieldModule, + MatInputModule, + BrowserAnimationsModule + ], declarations: [ ClassificationListComponent, ClassificationTypesSelectorStub, @@ -113,7 +126,7 @@ describe('ClassificationListComponent', () => { /* HTML: ACTION TOOLBAR */ it('should call CreateClassification when add-classification button is clicked', async () => { - const button = debugElement.nativeElement.querySelector('.add-classification-button'); + const button = debugElement.nativeElement.querySelector('.action-toolbar__add-button'); expect(button).toBeTruthy(); let actionDispatched = false; actions$.pipe(ofActionDispatched(CreateClassification)).subscribe(() => (actionDispatched = true)); @@ -125,7 +138,9 @@ describe('ClassificationListComponent', () => { expect(debugElement.nativeElement.querySelector('taskana-administration-import-export')).toBeTruthy(); }); - it('should display classification-types-selector component', () => { + it('should display classification-types-selector component when showFilter is true', () => { + component.showFilter = true; + fixture.detectChanges(); const typesSelectorComponent = debugElement.nativeElement.querySelector( 'taskana-administration-classification-types-selector' ); @@ -133,31 +148,43 @@ describe('ClassificationListComponent', () => { }); /* HTML: FILTER */ - it('should display specific icon when selectedCategory is true', () => { - component.selectedCategory = 'EXTERNAL'; + it('should display filter input field when showFilter is true', () => { + component.showFilter = true; fixture.detectChanges(); - expect(debugElement.nativeElement.querySelector('.selected-category')).toBeTruthy(); + const button = debugElement.nativeElement.querySelector('.filter__input-field'); + expect(button).toBeTruthy(); + expect(button.textContent).toBe('Filter classification'); }); - it('should display universal icon for categories when selectedCategory is false', () => { - expect(debugElement.nativeElement.querySelector('svg-icon.no-selected-category')).toBeTruthy(); + it('should display filter button when showFilter is true', () => { + component.showFilter = true; + fixture.detectChanges(); + const button = debugElement.nativeElement.querySelector('.category-filter__filter-button'); + expect(button).toBeTruthy(); + expect(button.textContent).toBe('filter_list'); }); it('should change selectedCategory property when button is clicked', () => { + component.showFilter = true; + fixture.detectChanges(); + const filterButton = debugElement.nativeElement.querySelector('.category-filter__filter-button'); + filterButton.click(); + fixture.detectChanges(); component.selectedCategory = 'EXTERNAL'; - const button = debugElement.nativeElement.querySelector('.category-all'); - button.click(); + const allButton = debugElement.query(By.css('.category-filter__all-button')); + expect(allButton).toBeTruthy(); + allButton.nativeElement.click(); expect(component.selectedCategory).toBe(''); }); it('should display list of categories which can be selected', () => { - expect(debugElement.nativeElement.querySelector('.category-all').textContent.trim()).toBe('All'); - - const categories = fixture.debugElement.nativeElement.getElementsByClassName('category-list'); - expect(categories.length).toBe(3); - expect(categories[0].textContent.trim()).toBe('EXTERNAL'); - expect(categories[1].textContent.trim()).toBe('MANUAL'); - expect(categories[2].textContent.trim()).toBe('AUTOMATIC'); + component.showFilter = true; + fixture.detectChanges(); + const filterButton = debugElement.nativeElement.querySelector('.category-filter__filter-button'); + filterButton.click(); + fixture.detectChanges(); + const matMenu = debugElement.queryAll(By.css('.category-filter__categories')); + expect(matMenu.length).toBe(4); }); /* HTML: CLASSIFICATION TREE */ @@ -172,10 +199,9 @@ describe('ClassificationListComponent', () => { }); it('should display icon and text when no classifications exist', () => { - const noClassifications = debugElement.nativeElement.querySelector('.no-classifications'); - expect(noClassifications.childNodes.length).toBe(2); + const noClassifications = debugElement.nativeElement.querySelector('.classification-list__no-items'); + expect(noClassifications.childNodes.length).toBe(1); expect(noClassifications.childNodes[0].textContent).toBe('There are no classifications'); - expect(noClassifications.childNodes[1].tagName).toBe('SVG-ICON'); }); /* TS: getCategoryIcon() */ diff --git a/web/src/app/administration/components/classification-list/classification-list.component.ts b/web/src/app/administration/components/classification-list/classification-list.component.ts index 8595be053..1cfa96dea 100644 --- a/web/src/app/administration/components/classification-list/classification-list.component.ts +++ b/web/src/app/administration/components/classification-list/classification-list.component.ts @@ -29,6 +29,7 @@ export class ClassificationListComponent implements OnInit, OnDestroy { requestInProgress = true; inputValue: string; selectedCategory = ''; + showFilter = false; @Select(ClassificationSelectors.classificationTypes) classificationTypes$: Observable; @Select(ClassificationSelectors.selectedClassificationType) classificationTypeSelected$: Observable; @@ -85,18 +86,25 @@ export class ClassificationListComponent implements OnInit, OnDestroy { this.location.go(this.location.path().replace(/(classifications).*/g, 'classifications/new-classification')); } + getCategoryIcon(category: string): Observable { + return this.categoryIcons$.pipe( + map((iconMap) => { + if (category === '') { + return new Pair(iconMap['all'], 'All'); + } + return iconMap[category] + ? new Pair(iconMap[category], category) + : new Pair(iconMap.missing, 'Category does not match with the configuration'); + }) + ); + } + selectCategory(category: string) { this.selectedCategory = category; } - getCategoryIcon(category: string): Observable { - return this.categoryIcons$.pipe( - map((iconMap) => - iconMap[category] - ? new Pair(iconMap[category], category) - : new Pair(iconMap.missing, 'Category does not match with the configuration') - ) - ); + displayFilter() { + this.showFilter = !this.showFilter; } ngOnDestroy() { diff --git a/web/src/app/administration/components/classification-overview/classification-overview.component.html b/web/src/app/administration/components/classification-overview/classification-overview.component.html index 27979c5fa..9de312344 100644 --- a/web/src/app/administration/components/classification-overview/classification-overview.component.html +++ b/web/src/app/administration/components/classification-overview/classification-overview.component.html @@ -1,10 +1,8 @@ -
    -
    - -
    -
    - -
    +
    + + +
    +