Skip to content

Commit

Permalink
Merged in bugfix/SHIBUI-1673 (pull request #450)
Browse files Browse the repository at this point in the history
SHIBUI-1673 Added page headings for all routes
  • Loading branch information
rmathis committed Dec 9, 2019
2 parents 27c117d + 222f82a commit e13a3da
Show file tree
Hide file tree
Showing 22 changed files with 271 additions and 46 deletions.
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ label.dynamic-attributes=Dynamic Attributes
label.metadata-filter-plugins=Metadata Filter Plugins
label.advanced-settings=Advanced Settings
label.edit-metadata-provider=Edit Metadata Provider
label.edit-metadata-source=Edit Metadata Source
label.http-settings-advanced=Http Settings (Advanced)

label.metadata-ui=User Interface / MDUI Information
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</div>
</nav>
<main class="pad-content">
<page-title class="sr-only sr-only-focusable"></page-title>
<router-outlet></router-outlet>
<notification-list></notification-list>
</main>
Expand Down
4 changes: 3 additions & 1 deletion ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { AppComponent } from './app.component';
import * as fromRoot from './core/reducer';
import { NotificationModule } from './notification/notification.module';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { MockTranslatePipe, MockI18nService, MockI18nModule } from '../testing/i18n.stub';
import { MockI18nService, MockI18nModule } from '../testing/i18n.stub';
import { I18nService } from './i18n/service/i18n.service';
import { NavigationService } from './core/service/navigation.service';
import { NavigationServiceStub } from '../testing/navigation-service.stub';
import { MockPageTitleComponent } from '../testing/page-title-component.stub';

@Component({
template: `
Expand Down Expand Up @@ -46,6 +47,7 @@ describe('AppComponent', () => {
],
declarations: [
AppComponent,
MockPageTitleComponent,
TestHostComponent
],
}).compileComponents();
Expand Down
1 change: 0 additions & 1 deletion ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export class AppComponent implements OnInit {
constructor(
private store: Store<fromRoot.State>,
private i18nService: I18nService,
private router: Router,
private navService: NavigationService
) {
this.version$ = this.store.select(fromRoot.getVersionInfo);
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreModule, Store } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store';
Expand All @@ -24,7 +24,6 @@ import { WizardModule } from './wizard/wizard.module';
import { FormModule } from './schema-form/schema-form.module';
import { environment } from '../environments/environment.prod';
import { I18nModule } from './i18n/i18n.module';
import { NavigationService } from './core/service/navigation.service';

@NgModule({
declarations: [
Expand All @@ -46,6 +45,7 @@ import { NavigationService } from './core/service/navigation.service';
}),
EffectsModule.forRoot([]),
BrowserModule,
CoreModule,
CoreModule.forRoot(),
StoreRouterConnectingModule.forRoot(),
NgbDropdownModule,
Expand Down Expand Up @@ -77,4 +77,4 @@ import { NavigationService } from './core/service/navigation.service';
],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {}
14 changes: 14 additions & 0 deletions ui/src/app/core/action/location.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Action } from '@ngrx/store';

export enum LocationActionTypes {
SET_TITLE = '[Location] Set Title'
}

export class SetTitle implements Action {
readonly type = LocationActionTypes.SET_TITLE;

constructor(public payload: string) { }
}

export type LocationActionUnion =
| SetTitle;
65 changes: 65 additions & 0 deletions ui/src/app/core/component/page-title.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, ViewChild } from '@angular/core';
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule, Store, combineReducers } from '@ngrx/store';

import * as fromWizard from '../../wizard/reducer';
import * as fromI18n from '../../i18n/reducer';
import * as fromCore from '../reducer';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { PageTitleComponent } from './page-title.component';
import { MockI18nModule, MockI18nService } from '../../../testing/i18n.stub';
import { I18nService } from '../../i18n/service/i18n.service';

@Component({
template: `<page-title></page-title>`
})
class TestHostComponent {
@ViewChild(PageTitleComponent, { static: true })
public componentUnderTest: PageTitleComponent;
}

describe('Page Title Component', () => {

let fixture: ComponentFixture<TestHostComponent>;
let instance: TestHostComponent;
let app: PageTitleComponent;
let store: Store<fromWizard.State>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbDropdownModule,
RouterTestingModule,
StoreModule.forRoot({
wizard: combineReducers(fromWizard.reducers),
core: combineReducers(fromCore.reducers),
i18n: combineReducers(fromI18n.reducers)
}),
MockI18nModule
],
declarations: [
PageTitleComponent,
TestHostComponent
],
providers: [
{
provide: I18nService,
useClass: MockI18nService
}
]
}).compileComponents();

store = TestBed.get(Store);
spyOn(store, 'dispatch');

fixture = TestBed.createComponent(TestHostComponent);
instance = fixture.componentInstance;
app = instance.componentUnderTest;
fixture.detectChanges();
}));

it('should compile without error', () => {
expect(app).toBeTruthy();
});
});
80 changes: 80 additions & 0 deletions ui/src/app/core/component/page-title.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component, OnInit, OnDestroy } from '@angular/core';

import { Store } from '@ngrx/store';

import * as fromRoot from '../reducer';
import { Observable, Subscription } from 'rxjs';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { I18nService } from '../../i18n/service/i18n.service';
import { filter, map, mergeMap, startWith, combineLatest } from 'rxjs/operators';
import { getCurrent } from '../../wizard/reducer';
import { getMessages } from '../../i18n/reducer';
import { SetTitle } from '../action/location.action';

@Component({
selector: 'page-title',
template: `<h2 tabindex="0" id="main-page-title" class="m-0">{{ pageTitle$ | async }}</h2>`,
styleUrls: []
})
export class PageTitleComponent implements OnInit, OnDestroy {

pageTitle$: Observable<string> = this.store.select(fromRoot.getLocationTitle);

initial = true;
sub: Subscription;

constructor(
private store: Store<fromRoot.State>,
private router: Router,
private activatedRoute: ActivatedRoute,
private translateService: I18nService
) {
this.pageTitle$.subscribe(title => {
const heading = document.getElementById('main-page-title');
if (heading && title) {
if (!this.initial) {
heading.focus();
}
this.initial = false;
}
if (title) {
document.title = `Shibboleth IDP UI | ${title}`;
}
});
}

ngOnInit(): void {
this.sub = this.router.events.pipe(
filter(e => e instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data),
combineLatest(
this.store.select(getCurrent).pipe(
filter(c => !!c),
startWith({ label: '' })
),
this.store.select(getMessages).pipe(
startWith({})
)
),
map(([data, currentWizardPage, messages]) => {
const title = data.subtitle && currentWizardPage ?
`${data.title} - ${this.translateService.translate(currentWizardPage.label, null, messages)}`
:
data.title;
return new SetTitle(title);
})
).subscribe(this.store);
}

ngOnDestroy(): void {
this.sub.unsubscribe();
}
}
5 changes: 4 additions & 1 deletion ui/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import { ModalService } from './service/modal.service';
import { DifferentialService } from './service/differential.service';
import { NavigatorService } from './service/navigator.service';
import { NavigationService } from './service/navigation.service';
import { PageTitleComponent } from './component/page-title.component';

export const COMPONENTS = [];
export const COMPONENTS = [
PageTitleComponent
];

@NgModule({
imports: [
Expand Down
4 changes: 3 additions & 1 deletion ui/src/app/core/reducer/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as fromIndex from './index';
import * as fromUser from './user.reducer';
import * as fromVersion from './version.reducer';
import * as fromLocation from './location.reducer';
import * as fromConfig from './configuration.reducer';
import { VersionInfo } from '../model/version';

describe('Core index reducers', () => {
const state: fromIndex.CoreState = {
user: fromUser.initialState as fromUser.UserState,
version: fromVersion.initialState as fromVersion.VersionState,
config: fromConfig.initialState as fromConfig.ConfigState
config: fromConfig.initialState as fromConfig.ConfigState,
location: fromLocation.initialState as fromLocation.LocationState
};
describe('getUserStateFn function', () => {
it('should return the user state', () => {
Expand Down
9 changes: 8 additions & 1 deletion ui/src/app/core/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
import * as fromUser from './user.reducer';
import * as fromVersion from './version.reducer';
import * as fromConfig from './configuration.reducer';
import * as fromLocation from './location.reducer';
import * as fromRoot from '../../app.reducer';

export interface CoreState {
user: fromUser.UserState;
version: fromVersion.VersionState;
config: fromConfig.ConfigState;
location: fromLocation.LocationState;
}

export interface State extends fromRoot.State {
Expand All @@ -21,13 +23,15 @@ export interface State extends fromRoot.State {
export const reducers = {
user: fromUser.reducer,
version: fromVersion.reducer,
config: fromConfig.reducer
config: fromConfig.reducer,
location: fromLocation.reducer
};

export const getCoreFeature = createFeatureSelector<CoreState>('core');
export const getUserStateFn = (state: CoreState) => state.user;
export const getVersionStateFn = (state: CoreState) => state.version;
export const getConfigStateFn = (state: CoreState) => state.config;
export const getLocationStateFn = (state: CoreState) => state.location;

export const getUserState = createSelector(getCoreFeature, getUserStateFn);
export const getUser = createSelector(getUserState, fromUser.getUser);
Expand All @@ -49,3 +53,6 @@ export const getUserRoles = createSelector(getRoles, filterRolesFn);
export const getCurrentUserRole = createSelector(getUser, u => u ? u.role : null);

export const isCurrentUserAdmin = createSelector(getUser, isUserAdminFn);

export const getLocationState = createSelector(getCoreFeature, getLocationStateFn);
export const getLocationTitle = createSelector(getLocationState, fromLocation.getTitle);
23 changes: 23 additions & 0 deletions ui/src/app/core/reducer/location.reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LocationActionUnion, LocationActionTypes } from '../action/location.action';

export interface LocationState {
title: string;
}

export const initialState: LocationState = {
title: ''
};

export function reducer(state = initialState, action: LocationActionUnion): LocationState {
switch (action.type) {
case LocationActionTypes.SET_TITLE:
return {
...state,
title: action.payload
};
default:
return state;
}
}

export const getTitle = (state: LocationState) => state.title;
19 changes: 15 additions & 4 deletions ui/src/app/dashboard/dashboard.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ const routes: Routes = [
component: ManagerComponent,
children: [
{ path: '', redirectTo: 'resolvers', pathMatch: 'prefix' },
{ path: 'resolvers', component: DashboardResolversListComponent },
{ path: 'providers', component: DashboardProvidersListComponent, canActivate: [AdminGuard] },
{
path: 'resolvers',
component: DashboardResolversListComponent,
data: { title: 'Metadata Source Dashboard' }
},
{
path: 'providers',
component: DashboardProvidersListComponent,
canActivate: [AdminGuard],
data: { title: 'Metadata Provider Dashboard' }
},
]
}
]
Expand All @@ -42,11 +51,13 @@ const routes: Routes = [
children: [
{
path: 'management',
component: AdminManagementPageComponent
component: AdminManagementPageComponent,
data: { title: 'User Administration Dashboard' }
},
{
path: 'actions',
component: ActionRequiredPageComponent
component: ActionRequiredPageComponent,
data: { title: 'Administrator Actions Required Dashboard' }
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/i18n/reducer/message.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface MessageState {

export const initialState: MessageState = {
fetching: false,
messages: null,
messages: {},
error: null,
locale: null
};
Expand Down
Loading

0 comments on commit e13a3da

Please sign in to comment.