diff --git a/ui/src/app/user/admin/action/collection.action.ts b/ui/src/app/user/admin/action/collection.action.ts new file mode 100644 index 000000000..96c2ae937 --- /dev/null +++ b/ui/src/app/user/admin/action/collection.action.ts @@ -0,0 +1,141 @@ +import { Action } from '@ngrx/store'; +import { Update } from '@ngrx/entity'; +import { Admin } from '../model/admin'; + +export enum AdminCollectionActionTypes { + SELECT_ADMIN_REQUEST = '[Admin Collection] Select Admin Request', + SELECT_ADMIN_SUCCESS = '[Admin Collection] Select Admin Success', + SELECT_ADMIN_FAIL = '[Admin Collection] Select Admin Fail', + + UPDATE_ADMIN_REQUEST = '[Admin Collection] Update Admin Request', + UPDATE_ADMIN_SUCCESS = '[Admin Collection] Update Admin Success', + UPDATE_ADMIN_FAIL = '[Admin Collection] Update Admin Fail', + + LOAD_ADMIN_REQUEST = '[Admin Collection] Load Admin Request', + LOAD_ADMIN_SUCCESS = '[Admin Collection] Load Admin Success', + LOAD_ADMIN_ERROR = '[Admin Collection] Load Admin Error', + + ADD_ADMIN_REQUEST = '[Admin Collection] Add Admin Request', + ADD_ADMIN_SUCCESS = '[Admin Collection] Add Admin Success', + ADD_ADMIN_FAIL = '[Admin Collection] Add Admin Fail', + + REMOVE_ADMIN_REQUEST = '[Admin Collection] Remove Admin Request', + REMOVE_ADMIN_SUCCESS = '[Admin Collection] Remove Admin Success', + REMOVE_ADMIN_FAIL = '[Admin Collection] Remove Admin Fail', + + CLEAR_ADMINS = '[Admin Collection] Clear Admins' + +} + +export class SelectAdmin implements Action { + readonly type = AdminCollectionActionTypes.SELECT_ADMIN_REQUEST; + + constructor(public payload: string) { } +} + +export class SelectAdminSuccess implements Action { + readonly type = AdminCollectionActionTypes.SELECT_ADMIN_SUCCESS; + + constructor(public payload: Admin) { } +} + +export class SelectAdminFail implements Action { + readonly type = AdminCollectionActionTypes.SELECT_ADMIN_FAIL; + + constructor(public payload: Error) { } +} + +export class LoadAdminRequest implements Action { + readonly type = AdminCollectionActionTypes.LOAD_ADMIN_REQUEST; + + constructor() { } +} + +export class LoadAdminSuccess implements Action { + readonly type = AdminCollectionActionTypes.LOAD_ADMIN_SUCCESS; + + constructor(public payload: Admin[]) { } +} + +export class LoadAdminError implements Action { + readonly type = AdminCollectionActionTypes.LOAD_ADMIN_ERROR; + + constructor(public payload: any) { } +} + +export class UpdateAdminRequest implements Action { + readonly type = AdminCollectionActionTypes.UPDATE_ADMIN_REQUEST; + + constructor(public payload: Admin) { } +} + +export class UpdateAdminSuccess implements Action { + readonly type = AdminCollectionActionTypes.UPDATE_ADMIN_SUCCESS; + + constructor(public payload: Update) { } +} + +export class UpdateAdminFail implements Action { + readonly type = AdminCollectionActionTypes.UPDATE_ADMIN_FAIL; + + constructor(public payload: Admin) { } +} + +export class AddAdminRequest implements Action { + readonly type = AdminCollectionActionTypes.ADD_ADMIN_REQUEST; + + constructor(public payload: Admin) { } +} + +export class AddAdminSuccess implements Action { + readonly type = AdminCollectionActionTypes.ADD_ADMIN_SUCCESS; + + constructor(public payload: Admin) { } +} + +export class AddAdminFail implements Action { + readonly type = AdminCollectionActionTypes.ADD_ADMIN_FAIL; + + constructor(public payload: any) { } +} + +export class RemoveAdminRequest implements Action { + readonly type = AdminCollectionActionTypes.REMOVE_ADMIN_REQUEST; + + constructor(public payload: string) { } +} + +export class RemoveAdminSuccess implements Action { + readonly type = AdminCollectionActionTypes.REMOVE_ADMIN_SUCCESS; + + constructor(public payload: string) { } +} + +export class RemoveAdminFail implements Action { + readonly type = AdminCollectionActionTypes.REMOVE_ADMIN_FAIL; + + constructor(public error: Error) { } +} + +export class ClearAdmins implements Action { + readonly type = AdminCollectionActionTypes.CLEAR_ADMINS; +} + + +export type AdminCollectionActionsUnion = + | LoadAdminRequest + | LoadAdminSuccess + | LoadAdminError + | AddAdminRequest + | AddAdminSuccess + | AddAdminFail + | RemoveAdminRequest + | RemoveAdminSuccess + | RemoveAdminFail + | SelectAdmin + | SelectAdminSuccess + | SelectAdminFail + | UpdateAdminRequest + | UpdateAdminSuccess + | UpdateAdminFail + | ClearAdmins; diff --git a/ui/src/app/user/admin/effect/collection.effect.ts b/ui/src/app/user/admin/effect/collection.effect.ts new file mode 100644 index 000000000..6120faf5b --- /dev/null +++ b/ui/src/app/user/admin/effect/collection.effect.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { switchMap, map } from 'rxjs/operators'; + +import * as fromAdmin from '../reducer'; +import { + LoadAdminRequest, + AdminCollectionActionTypes, + LoadAdminSuccess, + UpdateAdminRequest, + UpdateAdminSuccess, + RemoveAdminRequest, + RemoveAdminSuccess +} from '../action/collection.action'; +import { AdminService } from '../service/admin.service'; + + +/* istanbul ignore next */ +@Injectable() +export class AdminCollectionEffects { + + @Effect() + loadAdminRequest$ = this.actions$.pipe( + ofType(AdminCollectionActionTypes.LOAD_ADMIN_REQUEST), + switchMap(() => this.adminService.query().pipe( + map(users => new LoadAdminSuccess(users)) + )) + ); + + @Effect() + updateAdminRequest$ = this.actions$.pipe( + ofType(AdminCollectionActionTypes.UPDATE_ADMIN_REQUEST), + map(action => action.payload), + switchMap(changes => this.adminService.update(changes).pipe( + map(user => new UpdateAdminSuccess({ + id: changes.resourceId, + changes + })) + )) + ); + + @Effect() + removeAdminRequest$ = this.actions$.pipe( + ofType(AdminCollectionActionTypes.REMOVE_ADMIN_REQUEST), + map(action => action.payload), + switchMap(id => this.adminService.remove(id).pipe( + map(user => new RemoveAdminSuccess(id)) + )) + ); + + @Effect() + removeAdminSuccessReload$ = this.actions$.pipe( + ofType(AdminCollectionActionTypes.REMOVE_ADMIN_SUCCESS), + map(action => new LoadAdminRequest()) + ); + + constructor( + private actions$: Actions, + private adminService: AdminService, + private store: Store + ) { } +} diff --git a/ui/src/app/user/admin/model/admin.ts b/ui/src/app/user/admin/model/admin.ts new file mode 100644 index 000000000..5a5a9400a --- /dev/null +++ b/ui/src/app/user/admin/model/admin.ts @@ -0,0 +1,9 @@ +import { User } from '../../../core/model/user'; + +export interface Admin extends User { + createdDate?: string; + updatedDate?: string; + resourceId: string; + + email: string; +} diff --git a/ui/src/app/user/admin/reducer/collection.reducer.spec.ts b/ui/src/app/user/admin/reducer/collection.reducer.spec.ts new file mode 100644 index 000000000..df4d6df3e --- /dev/null +++ b/ui/src/app/user/admin/reducer/collection.reducer.spec.ts @@ -0,0 +1,83 @@ +import { reducer, initialState as snapshot } from './collection.reducer'; +import * as fromAdmin from './collection.reducer'; +import { + AdminCollectionActionTypes, + LoadAdminSuccess, + UpdateAdminSuccess, + RemoveAdminSuccess +} from '../action/collection.action'; +import { Admin } from '../model/admin'; + +let users = [ + { + resourceId: 'abc', + role: 'SUPER_ADMIN', + email: 'foo@bar.com', + name: { + first: 'Jane', + last: 'Doe' + } + }, + { + resourceId: 'def', + role: 'DELEGATED_ADMIN', + email: 'bar@baz.com', + name: { + first: 'John', + last: 'Doe' + } + } +]; + +describe('Admin Collection Reducer', () => { + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(snapshot, {} as any); + + expect(result).toEqual(snapshot); + }); + }); + + describe(`${AdminCollectionActionTypes.LOAD_ADMIN_SUCCESS}`, () => { + it('should add the loaded filters to the collection', () => { + spyOn(fromAdmin.adapter, 'addAll').and.callThrough(); + const action = new LoadAdminSuccess(users); + const result = reducer(snapshot, action); + expect(fromAdmin.adapter.addAll).toHaveBeenCalled(); + }); + }); + + describe(`${AdminCollectionActionTypes.UPDATE_ADMIN_SUCCESS}`, () => { + it('should update the filter in the collection', () => { + spyOn(fromAdmin.adapter, 'updateOne').and.callThrough(); + const update = { + id: 'abc', + changes: { role: 'DELEGATED_ADMIN' } + }; + const action = new UpdateAdminSuccess(update); + const result = reducer(snapshot, action); + expect(fromAdmin.adapter.updateOne).toHaveBeenCalled(); + }); + }); + + describe(`${AdminCollectionActionTypes.REMOVE_ADMIN_SUCCESS}`, () => { + it('should set saving to false', () => { + const action = new RemoveAdminSuccess('abc'); + expect(reducer(snapshot, action).saving).toBe(false); + }); + }); + + describe('selector methods', () => { + describe('getSelectedAdminId', () => { + it('should return the state selectedAdminId', () => { + expect(fromAdmin.getSelectedAdminId(snapshot)).toBe(snapshot.selectedAdminId); + }); + }); + + describe('getError', () => { + it('should return the state saving', () => { + expect(fromAdmin.getIsSaving(snapshot)).toBe(snapshot.saving); + }); + }); + }); +}); diff --git a/ui/src/app/user/admin/reducer/collection.reducer.ts b/ui/src/app/user/admin/reducer/collection.reducer.ts new file mode 100644 index 000000000..1e884b907 --- /dev/null +++ b/ui/src/app/user/admin/reducer/collection.reducer.ts @@ -0,0 +1,54 @@ +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; +import { Admin } from '../model/admin'; +import { AdminCollectionActionsUnion, AdminCollectionActionTypes } from '../action/collection.action'; + +export interface CollectionState extends EntityState { + selectedAdminId: string | null; + saving: boolean; +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (model: Admin) => model.resourceId +}); + +export const initialState: CollectionState = adapter.getInitialState({ + selectedAdminId: null, + saving: false +}); + +export function reducer(state = initialState, action: AdminCollectionActionsUnion): CollectionState { + switch (action.type) { + case AdminCollectionActionTypes.LOAD_ADMIN_SUCCESS: { + let s = adapter.addAll(action.payload, { + ...state, + selectedAdminId: state.selectedAdminId + }); + return s; + } + case AdminCollectionActionTypes.UPDATE_ADMIN_SUCCESS: { + return adapter.updateOne(action.payload, { + ...state, + saving: false + }); + } + case AdminCollectionActionTypes.REMOVE_ADMIN_SUCCESS: { + return adapter.removeOne(action.payload, { + ...state, + saving: false + }); + } + + default: { + return state; + } + } +} + +export const getSelectedAdminId = (state: CollectionState) => state.selectedAdminId; +export const getIsSaving = (state: CollectionState) => state.saving; +export const { + selectIds: selectAdminIds, + selectEntities: selectAdminEntities, + selectAll: selectAllAdmins, + selectTotal: selectAdminTotal +} = adapter.getSelectors(); diff --git a/ui/src/app/user/admin/reducer/index.ts b/ui/src/app/user/admin/reducer/index.ts new file mode 100644 index 000000000..ad9dfd93f --- /dev/null +++ b/ui/src/app/user/admin/reducer/index.ts @@ -0,0 +1,33 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromRoot from '../../../core/reducer'; +import * as fromCollection from './collection.reducer'; + +export interface AdminState { + collection: fromCollection.CollectionState; +} + +export const reducers = { + collection: fromCollection.reducer +}; + +export interface State extends fromRoot.State { + 'admin': AdminState; +} + +export const getCollectionFromStateFn = (state: AdminState) => state.collection; + +export const getAdminState = createFeatureSelector('admin'); + +/* + * Select pieces of Admin Collection +*/ +export const getCollectionState = createSelector(getAdminState, getCollectionFromStateFn); +export const getAllAdmins = createSelector(getCollectionState, fromCollection.selectAllAdmins); +export const getCollectionSaving = createSelector(getCollectionState, fromCollection.getIsSaving); + +export const getAdminEntities = createSelector(getCollectionState, fromCollection.selectAdminEntities); +export const getSelectedAdminId = createSelector(getCollectionState, fromCollection.getSelectedAdminId); +export const getSelectedAdmin = createSelector(getAdminEntities, getSelectedAdminId, (entities, selectedId) => { + return selectedId && entities[selectedId]; +}); +export const getAdminIds = createSelector(getCollectionState, fromCollection.selectAdminIds); diff --git a/ui/src/app/user/admin/service/admin.service.spec.ts b/ui/src/app/user/admin/service/admin.service.spec.ts new file mode 100644 index 000000000..77b1a27ea --- /dev/null +++ b/ui/src/app/user/admin/service/admin.service.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { HttpModule } from '@angular/http'; +import { AdminService } from './admin.service'; + +describe('Admin Service', () => { + let service: AdminService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpModule], + providers: [ + AdminService + ] + }); + service = TestBed.get(AdminService); + }); + + it('should instantiate', () => { + expect(service).toBeDefined(); + }); + + describe('query method', () => { + it('should return a list of users', () => { + expect(true).toBe(false); + }); + }); + describe('update method', () => { + it('should send an http put request', () => { + expect(true).toBe(false); + }); + }); + describe('remove method', () => { + it('should send an http delete request', () => { + expect(true).toBe(false); + }); + }); +}); diff --git a/ui/src/app/user/admin/service/admin.service.ts b/ui/src/app/user/admin/service/admin.service.ts new file mode 100644 index 000000000..77bd39c05 --- /dev/null +++ b/ui/src/app/user/admin/service/admin.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Admin } from '../model/admin'; + +let users = [ + { + resourceId: 'abc', + role: 'SUPER_ADMIN', + email: 'foo@bar.com', + name: { + first: 'Jane', + last: 'Doe' + } + }, + { + resourceId: 'def', + role: 'DELEGATED_ADMIN', + email: 'bar@baz.com', + name: { + first: 'John', + last: 'Doe' + } + } +]; + +@Injectable() +export class AdminService { + + constructor() { } + query(): Observable { + return of([ + ...users + ]); + } + + update(user: Admin): Observable { + return of({ + ...users.find(u => u.resourceId === user.resourceId), + ...user + }); + } + + remove(userId: string): Observable { + users = users.filter(u => u.resourceId !== userId); + return of(true); + } +}