diff --git a/ui/package-lock.json b/ui/package-lock.json index 5ff0025d3..82dfa6612 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1716,7 +1716,7 @@ }, "compression": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "dev": true, "requires": { diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index 1d3703df5..62dabd575 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -1,41 +1,78 @@ -import { TestBed, async } from '@angular/core/testing'; +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 { AppComponent } from './app.component'; import { User } from './core/model/user'; import * as fromRoot from './core/reducer'; +import * as fromVersion from './core/reducer/version.reducer'; import { NotificationModule } from './notification/notification.module'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(AppComponent) + public componentUnderTest: AppComponent; +} + describe('AppComponent', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: AppComponent; let store: Store; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ NgbDropdownModule.forRoot(), RouterTestingModule, - StoreModule.forRoot({}), + StoreModule.forRoot({ + core: combineReducers(fromRoot.reducers) + }), NotificationModule ], declarations: [ - AppComponent + AppComponent, + TestHostComponent ], }).compileComponents(); store = TestBed.get(Store); spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); })); it('should create the app', async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); + expect(store.dispatch).toHaveBeenCalledTimes(4); })); it(`should have as title 'Shib-UI'`, async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; expect(app.title).toEqual('Shib UI'); })); + + describe('version format', () => { + it('should return a formatted string', () => { + expect(app.formatter({ + build: { + version: 'foo' + }, + git: { + commit: { + id: 'bar' + } + } + })).toEqual('foo-bar'); + }); + }); }); diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 12f14544a..f3d02e3a0 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -21,9 +21,11 @@ export class AppComponent implements OnInit { formatted$: Observable; today = new Date(); + formatter = v => v && v.build ? `${v.build.version}-${v.git.commit.id}` : ''; + constructor(private store: Store) { this.version$ = this.store.select(fromRoot.getVersionInfo); - this.formatted$ = this.version$.map(v => v && v.build ? `${v.build.version}-${v.git.commit.id}` : ''); + this.formatted$ = this.version$.map(this.formatter); } ngOnInit(): void { diff --git a/ui/src/app/domain/model/mdui.ts b/ui/src/app/domain/model/mdui.ts index d31287b56..6e309f3f1 100644 --- a/ui/src/app/domain/model/mdui.ts +++ b/ui/src/app/domain/model/mdui.ts @@ -6,4 +6,4 @@ export interface MDUI { logoHeight?: number; logoWidth?: number; description?: string; -} \ No newline at end of file +} diff --git a/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts b/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts index 0fdfa326a..687c54995 100644 --- a/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts +++ b/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts @@ -1,6 +1,14 @@ import { reducer } from './filter-collection.reducer'; import * as fromFilter from './filter-collection.reducer'; import * as actions from '../action/filter-collection.action'; +import { + FilterCollectionActionsUnion, + FilterCollectionActionTypes, + LoadFilterSuccess, + UpdateFilterSuccess, + SelectFilter +} from '../action/filter-collection.action'; +import { Filter } from '../entity/filter'; const snapshot: fromFilter.FilterCollectionState = { ids: [], @@ -17,4 +25,39 @@ describe('Filter Reducer', () => { expect(result).toEqual(snapshot); }); }); + + describe(`${FilterCollectionActionTypes.SELECT}`, () => { + it('should set the selected id in the store', () => { + const selectedFilterId = 'foo'; + const action = new SelectFilter(selectedFilterId); + const result = reducer(snapshot, action); + expect(result).toEqual({...snapshot, selectedFilterId}); + }); + }); + + describe(`${FilterCollectionActionTypes.LOAD_FILTER_SUCCESS}`, () => { + it('should add the loaded filters to the collection', () => { + spyOn(fromFilter.adapter, 'addMany').and.callThrough(); + const filters = [ + new Filter({ id: 'foo', createdDate: new Date().toLocaleDateString() }), + new Filter({ id: 'bar', createdDate: new Date().toLocaleDateString() }) + ]; + const action = new LoadFilterSuccess(filters); + const result = reducer(snapshot, action); + expect(fromFilter.adapter.addMany).toHaveBeenCalled(); + }); + }); + + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS}`, () => { + it('should add the loaded filters to the collection', () => { + spyOn(fromFilter.adapter, 'updateOne').and.callThrough(); + const update = { + id: 'foo', + changes: new Filter({ id: 'foo', filterName: 'bar', createdDate: new Date().toLocaleDateString() }), + }; + const action = new UpdateFilterSuccess(update); + const result = reducer(snapshot, action); + expect(fromFilter.adapter.updateOne).toHaveBeenCalled(); + }); + }); }); diff --git a/ui/src/app/domain/service/entity-descriptor.service.ts b/ui/src/app/domain/service/entity-descriptor.service.ts index ec0b38b46..b42711431 100644 --- a/ui/src/app/domain/service/entity-descriptor.service.ts +++ b/ui/src/app/domain/service/entity-descriptor.service.ts @@ -39,9 +39,6 @@ export class EntityDescriptorService { } save(provider: MetadataProvider): Observable { - if (!environment.production) { - // console.log(JSON.stringify(provider)); - } return this.http.post(`${this.base}${this.endpoint}`, provider); } diff --git a/ui/src/app/metadata-filter/container/edit-filter.component.html b/ui/src/app/metadata-filter/container/edit-filter.component.html index dd7ba29f7..f65a6cc0d 100644 --- a/ui/src/app/metadata-filter/container/edit-filter.component.html +++ b/ui/src/app/metadata-filter/container/edit-filter.component.html @@ -37,7 +37,7 @@ - Filter Name is required + Filter Name is required @@ -56,7 +56,7 @@ aria-label="Information icon - press spacebar to read additional information for this form field"> - Search Entity ID + Search Entity ID - + Minimum 4 characters. diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts index 2b42917c2..a61f8e0f8 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts @@ -1,6 +1,29 @@ import { reducer } from './filter.reducer'; import * as fromFilter from './filter.reducer'; import * as actions from '../action/filter.action'; +import * as searchActions from '../action/search.action'; +import { + SelectId, + LoadEntityPreviewSuccess, + UpdateFilterChanges, + CancelCreateFilter +} from '../action/filter.action'; + +import { + ClearSearch +} from '../action/search.action'; + +import { + FilterCollectionActionsUnion, + FilterCollectionActionTypes, + AddFilterRequest, + UpdateFilterRequest, + AddFilterSuccess, + UpdateFilterSuccess +} from '../../domain/action/filter-collection.action'; +import { MDUI } from '../../domain/model/mdui'; +import { MetadataFilter } from '../../domain/domain.type'; +import { Filter } from '../../domain/entity/filter'; const snapshot: fromFilter.FilterState = { selected: null, @@ -9,6 +32,16 @@ const snapshot: fromFilter.FilterState = { saving: false }; +const mdui: MDUI = { + displayName: 'foo', + informationUrl: 'bar', + privacyStatementUrl: 'baz', + logoUrl: '', + logoHeight: 100, + logoWidth: 100, + description: '', +}; + describe('Filter Reducer', () => { describe('undefined action', () => { it('should return the default state', () => { @@ -17,4 +50,68 @@ describe('Filter Reducer', () => { expect(result).toEqual(snapshot); }); }); + + describe(`${actions.SELECT_ID} action`, () => { + it('should set selected property to the provided payload', () => { + const id = 'foo'; + const result = reducer(snapshot, new actions.SelectId(id)); + expect(result.selected).toBe(id); + }); + }); + + describe(`${actions.LOAD_ENTITY_PREVIEW_SUCCESS} action`, () => { + it('should set preview property to the provided payload', () => { + let sampleMdui = { ...mdui }; + const result = reducer(snapshot, new actions.LoadEntityPreviewSuccess(sampleMdui)); + expect(result.preview).toEqual(sampleMdui); + }); + }); + + describe(`${actions.UPDATE_FILTER} action`, () => { + it('should update the state of changes', () => { + const changes = { filterEnabled: false }; + const current = { ...snapshot, changes: { filterEnabled: true } as MetadataFilter }; + const result = reducer(current, new actions.UpdateFilterChanges(changes)); + expect(result.changes.filterEnabled).toBe(false); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER} action`, () => { + it('should set saving to true', () => { + const result = reducer(snapshot, new AddFilterRequest(new Filter())); + expect(result.saving).toBe(true); + }); + }); + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_REQUEST} action`, () => { + it('should set saving to true', () => { + const result = reducer(snapshot, new UpdateFilterRequest(new Filter())); + expect(result.saving).toBe(true); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER_SUCCESS} action`, () => { + it('should set saving to true', () => { + const result = reducer(snapshot, new AddFilterSuccess(new Filter())); + expect(result).toEqual(fromFilter.initialState); + }); + }); + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS} action`, () => { + it('should set saving to true', () => { + const update = {id: 'foo', changes: new Filter({id: 'foo'})}; + const result = reducer(snapshot, new UpdateFilterSuccess(update)); + expect(result).toEqual(fromFilter.initialState); + }); + }); + describe(`${searchActions.CLEAR_SEARCH} action`, () => { + it('should set saving to true', () => { + const result = reducer(snapshot, new ClearSearch()); + expect(result).toEqual(fromFilter.initialState); + }); + }); + describe(`${actions.CANCEL_CREATE_FILTER} action`, () => { + it('should set saving to true', () => { + const result = reducer(snapshot, new CancelCreateFilter()); + expect(result).toEqual(fromFilter.initialState); + }); + }); }); diff --git a/ui/src/app/metadata-provider/action/copy.action.ts b/ui/src/app/metadata-provider/action/copy.action.ts new file mode 100644 index 000000000..198a976db --- /dev/null +++ b/ui/src/app/metadata-provider/action/copy.action.ts @@ -0,0 +1,44 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../../domain/model/metadata-provider'; + +export enum CopySourceActionTypes { + CREATE_PROVIDER_COPY_REQUEST = '[Copy Provider] Create Provider Copy Request', + CREATE_PROVIDER_COPY_SUCCESS = '[Copy Provider] Create Provider Copy Success', + CREATE_PROVIDER_COPY_ERROR = '[Copy Provider] Create Provider Copy Error', + + UPDATE_PROVIDER_COPY = '[Copy Provider] Update Provider Copy Request', + + SAVE_PROVIDER_COPY_REQUEST = '[Copy Provider] Save Provider Copy Request', + SAVE_PROVIDER_COPY_SUCCESS = '[Copy Provider] Save Provider Copy Request', + SAVE_PROVIDER_COPY_ERROR = '[Copy Provider] Save Provider Copy Request', +} + +export class CreateProviderCopyRequest implements Action { + readonly type = CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST; + + constructor(public payload: { entityId: string, serviceProviderName: string, target: string }) { } +} + +export class CreateProviderCopySuccess implements Action { + readonly type = CopySourceActionTypes.CREATE_PROVIDER_COPY_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class CreateProviderCopyError implements Action { + readonly type = CopySourceActionTypes.CREATE_PROVIDER_COPY_ERROR; + + constructor(public payload: Error) { } +} + +export class UpdateProviderCopy implements Action { + readonly type = CopySourceActionTypes.UPDATE_PROVIDER_COPY; + + constructor(public payload: Partial) { } +} + +export type CopySourceActionUnion = + | CreateProviderCopyRequest + | CreateProviderCopySuccess + | CreateProviderCopyError + | UpdateProviderCopy; diff --git a/ui/src/app/metadata-provider/action/search.action.ts b/ui/src/app/metadata-provider/action/search.action.ts new file mode 100644 index 000000000..e4623c8c9 --- /dev/null +++ b/ui/src/app/metadata-provider/action/search.action.ts @@ -0,0 +1,29 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../../domain/model/metadata-provider'; + +export enum SearchActionTypes { + SEARCH_IDS = '[Search Provider Ids] Request', + SEARCH_IDS_SUCCESS = '[Search Provider Ids] Success', + SEARCH_IDS_ERROR = '[Search Provider Ids] Error' +} + +export class SearchIds implements Action { + readonly type = SearchActionTypes.SEARCH_IDS; + + constructor(public payload: string) { } +} +export class SearchIdsSuccess implements Action { + readonly type = SearchActionTypes.SEARCH_IDS_SUCCESS; + + constructor(public payload: string[]) { } +} +export class SearchIdsError implements Action { + readonly type = SearchActionTypes.SEARCH_IDS_ERROR; + + constructor(public payload: any) { } +} + +export type SearchActionUnion = + | SearchIds + | SearchIdsSuccess + | SearchIdsError; diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts b/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts index a46bff536..e6f1e2eec 100644 --- a/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts +++ b/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts @@ -22,9 +22,7 @@ describe('Blank Provider Page', () => { ReactiveFormsModule, ], declarations: [ - NewProviderComponent, - BlankProviderComponent, - UploadProviderComponent + BlankProviderComponent ], }); diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.ts b/ui/src/app/metadata-provider/container/blank-provider.component.ts index 85865571c..2d6d91414 100644 --- a/ui/src/app/metadata-provider/container/blank-provider.component.ts +++ b/ui/src/app/metadata-provider/container/blank-provider.component.ts @@ -18,6 +18,8 @@ import { AddDraftRequest } from '../../domain/action/draft-collection.action'; import { AddProviderRequest, UploadProviderRequest } from '../../domain/action/provider-collection.action'; import * as fromCollections from '../../domain/reducer'; import { EntityValidators } from '../../domain/service/entity-validators.service'; +import { MetadataProvider } from '../../domain/domain.type'; +import { Provider } from '../../domain/entity/provider'; @Component({ selector: 'blank-provider-form', @@ -44,9 +46,10 @@ export class BlankProviderComponent implements OnInit { } next(): void { - this.save.emit({ + const val: MetadataProvider = new Provider({ entityId: this.providerForm.get('entityId').value, serviceProviderName: this.providerForm.get('serviceProviderName').value }); + this.store.dispatch(new AddDraftRequest(val)); } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/confirm-copy.component.html b/ui/src/app/metadata-provider/container/confirm-copy.component.html new file mode 100644 index 000000000..e9c04eae6 --- /dev/null +++ b/ui/src/app/metadata-provider/container/confirm-copy.component.html @@ -0,0 +1,42 @@ + + + + + + Add a new metadata source - Finish Summary + + + + + + + + + + Back + + + Name and Entity ID. + + + + + + + FINISH SUMMARY AND VALIDATION + + + + + Save + + + Save + + + + + + + + diff --git a/ui/src/app/metadata-provider/container/confirm-copy.component.scss b/ui/src/app/metadata-provider/container/confirm-copy.component.scss new file mode 100644 index 000000000..9b93e80de --- /dev/null +++ b/ui/src/app/metadata-provider/container/confirm-copy.component.scss @@ -0,0 +1,5 @@ +:host { + .nav.nav-wizard .nav-item .nav-link.btn.save { + min-width: 180px; + } +} \ No newline at end of file diff --git a/ui/src/app/metadata-provider/container/confirm-copy.component.spec.ts b/ui/src/app/metadata-provider/container/confirm-copy.component.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata-provider/container/confirm-copy.component.ts b/ui/src/app/metadata-provider/container/confirm-copy.component.ts new file mode 100644 index 000000000..fdf0956b9 --- /dev/null +++ b/ui/src/app/metadata-provider/container/confirm-copy.component.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { Observable } from 'rxjs/Observable'; + +import * as fromProvider from '../reducer'; +import { MetadataProvider } from '../../domain/domain.type'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; +import { UpdateProviderCopy } from '../action/copy.action'; +import { map, take } from 'rxjs/operators'; +import { AddProviderRequest } from '../../domain/action/provider-collection.action'; + +@Component({ + selector: 'confirm-copy-page', + templateUrl: './confirm-copy.component.html', + styleUrls: ['./confirm-copy.component.scss'] +}) +export class ConfirmCopyComponent { + + copy$: Observable; + values$: Observable; + saving$: Observable; + + provider: MetadataProvider; + + constructor( + private store: Store, + private valueEmitter: ProviderValueEmitter + ) { + this.copy$ = this.store.select(fromProvider.getCopy); + this.saving$ = this.store.select(fromProvider.getSaving); + + this.values$ = this.copy$.pipe(take(1)); + this.valueEmitter.changeEmitted$.subscribe(changes => this.store.dispatch(new UpdateProviderCopy(changes))); + + this.copy$.subscribe(p => this.provider = p); + } + + onSave(provider: MetadataProvider): void { + this.store.dispatch(new AddProviderRequest(provider)); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.html b/ui/src/app/metadata-provider/container/copy-provider.component.html new file mode 100644 index 000000000..07bfe98f1 --- /dev/null +++ b/ui/src/app/metadata-provider/container/copy-provider.component.html @@ -0,0 +1,75 @@ + + + + + 1 + 1. Name and EntityId + + + + + + FINISH AND VALIDATE + + + + Next + + + + + + + + Select the Entity ID to copy + + + + + + + + + + + + + Metadata Source Name (Dashboard Display Only) + + + + + + Service Provider Name is required + + + + + + New Entity ID + + + + + + Entity ID is required + + + + Entity ID must be unique + + + + Next + + + + \ No newline at end of file diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts b/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts new file mode 100644 index 000000000..54981361c --- /dev/null +++ b/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts @@ -0,0 +1,48 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NewProviderComponent } from './new-provider.component'; +import * as fromCollections from '../../domain/reducer'; +import * as fromProvider from '../reducer'; +import { CopyProviderComponent } from './copy-provider.component'; +import { SharedModule } from '../../shared/shared.module'; +import { NavigatorService } from '../../core/service/navigator.service'; + +describe('Copy Provider Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: CopyProviderComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + collections: combineReducers(fromCollections.reducers), + provider: combineReducers(fromProvider.reducers) + }), + ReactiveFormsModule, + SharedModule + ], + declarations: [ + CopyProviderComponent + ], + providers: [ + NavigatorService + ] + }); + + fixture = TestBed.createComponent(CopyProviderComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.ts b/ui/src/app/metadata-provider/container/copy-provider.component.ts new file mode 100644 index 000000000..9c3ae4182 --- /dev/null +++ b/ui/src/app/metadata-provider/container/copy-provider.component.ts @@ -0,0 +1,69 @@ +import { + Component, + OnInit, + Output, + EventEmitter +} from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; + +import { startWith, take } from 'rxjs/operators'; + +import { AddDraftRequest } from '../../domain/action/draft-collection.action'; +import { AddProviderRequest, UploadProviderRequest } from '../../domain/action/provider-collection.action'; +import * as fromCollections from '../../domain/reducer'; +import { EntityValidators } from '../../domain/service/entity-validators.service'; +import { SearchIds } from '../action/search.action'; +import * as fromProvider from '../reducer'; +import { Provider } from '../../domain/entity/provider'; +import { CreateProviderCopyRequest } from '../action/copy.action'; + + +@Component({ + selector: 'copy-provider-form', + templateUrl: './copy-provider.component.html' +}) +export class CopyProviderComponent implements OnInit { + @Output() save: EventEmitter = new EventEmitter(); + + providerForm: FormGroup; + ids$: Observable; + searchResults$: Observable; + + constructor( + private store: Store, + private fb: FormBuilder + ) { + this.ids$ = this.store.select(fromCollections.getAllEntityIds); + this.searchResults$ = this.store.select(fromProvider.getSearchResults); + } + + ngOnInit(): void { + this.providerForm = this.fb.group({ + serviceProviderName: ['', [Validators.required]], + entityId: ['', Validators.required, EntityValidators.createUniqueIdValidator(this.ids$)], + target: ['', [Validators.required], [EntityValidators.existsInCollection(this.ids$)]] + }); + + this.store.select(fromProvider.getAttributes) + .pipe(take(1)) + .subscribe(attrs => this.providerForm.setValue({ ...attrs })); + + this.providerForm + .get('target') + .valueChanges + .subscribe(val => { + this.store.dispatch(new SearchIds(val)); + }); + } + + next(): void { + this.store.dispatch(new CreateProviderCopyRequest({ + ...this.providerForm.value + })); + } + + updateOptions(query: string): void {} +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/new-provider.component.html b/ui/src/app/metadata-provider/container/new-provider.component.html index f32d9a8ce..8c6d5204f 100644 --- a/ui/src/app/metadata-provider/container/new-provider.component.html +++ b/ui/src/app/metadata-provider/container/new-provider.component.html @@ -11,29 +11,52 @@ How are you adding the metadata information? - - - Upload/URL - - - - - or - - - - Create File - - - - - - - - - + + + + + Upload/URL + + + + + or + + + + Create + + + + + or + + + + Copy + + + + + + + + diff --git a/ui/src/app/metadata-provider/container/new-provider.component.scss b/ui/src/app/metadata-provider/container/new-provider.component.scss new file mode 100644 index 000000000..ac956e4e7 --- /dev/null +++ b/ui/src/app/metadata-provider/container/new-provider.component.scss @@ -0,0 +1,5 @@ +:host { + .provider-nav-option { + width: 160px; + } +} \ No newline at end of file diff --git a/ui/src/app/metadata-provider/container/new-provider.component.spec.ts b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts index 3ac3a9e16..e81de97f1 100644 --- a/ui/src/app/metadata-provider/container/new-provider.component.spec.ts +++ b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts @@ -1,16 +1,28 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule, Router, ActivatedRoute } from '@angular/router'; +import { APP_BASE_HREF } from '@angular/common'; + import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { NewProviderComponent } from './new-provider.component'; -import * as fromCollections from '../../domain/reducer'; + import { BlankProviderComponent } from './blank-provider.component'; import { UploadProviderComponent } from './upload-provider.component'; +import { CopyProviderComponent } from './copy-provider.component'; +import { SharedModule } from '../../shared/shared.module'; +import { NavigatorService } from '../../core/service/navigator.service'; +import * as fromProvider from '../reducer'; +import * as fromCollections from '../../domain/reducer'; +import { RouterStub } from '../../../testing/router.stub'; +import { ActivatedRouteStub } from '../../../testing/activated-route.stub'; describe('New Provider Page', () => { let fixture: ComponentFixture; let store: Store; let instance: NewProviderComponent; + let activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); + activatedRoute.testParamMap = { id: 'foo' }; beforeEach(() => { TestBed.configureTestingModule({ @@ -18,14 +30,23 @@ describe('New Provider Page', () => { NoopAnimationsModule, StoreModule.forRoot({ collections: combineReducers(fromCollections.reducers), + provider: combineReducers(fromProvider.reducers) }), ReactiveFormsModule, + SharedModule, + RouterModule.forRoot([]) ], declarations: [ NewProviderComponent, BlankProviderComponent, - UploadProviderComponent + UploadProviderComponent, + CopyProviderComponent ], + providers: [ + NavigatorService, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: APP_BASE_HREF, useValue: '/' } + ] }); fixture = TestBed.createComponent(NewProviderComponent); diff --git a/ui/src/app/metadata-provider/container/new-provider.component.ts b/ui/src/app/metadata-provider/container/new-provider.component.ts index 43457f430..4dee1bf02 100644 --- a/ui/src/app/metadata-provider/container/new-provider.component.ts +++ b/ui/src/app/metadata-provider/container/new-provider.component.ts @@ -1,62 +1,11 @@ -import { - Component, - OnChanges, - OnInit, - OnDestroy, - ElementRef, - ViewChildren -} from '@angular/core'; -import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/take'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { Store } from '@ngrx/store'; - -import { MetadataProvider } from '../../domain/model/metadata-provider'; -import { Provider } from '../../domain/entity/provider'; -import { AddDraftRequest } from '../../domain/action/draft-collection.action'; -import { AddProviderRequest, UploadProviderRequest, CreateProviderFromUrlRequest } from '../../domain/action/provider-collection.action'; -import * as fromCollections from '../../domain/reducer'; -import { EntityValidators } from '../../domain/service/entity-validators.service'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'new-provider-page', - templateUrl: './new-provider.component.html' + templateUrl: './new-provider.component.html', + styleUrls: ['./new-provider.component.scss'] }) -export class NewProviderComponent implements OnInit { - private ngUnsubscribe: Subject = new Subject(); - - readonly UPLOAD = Symbol('UPLOAD_FORM'); - readonly BLANK = Symbol('BLANK_FORM'); - - type: Symbol = this.BLANK; - - constructor( - private store: Store - ) { } - - ngOnInit(): void { - this.toggle(this.type); - } - - toggle(type: Symbol): void { - this.type = type; - } - - upload(uploadFile: { name: string, body: string }): void { - this.store.dispatch(new UploadProviderRequest(uploadFile)); - } - - createFromUrl(data: { name: string, url: string }): void { - this.store.dispatch(new CreateProviderFromUrlRequest(data)); - } - - next(provider: { entityId: string, serviceProviderName: string }): void { - const val: MetadataProvider = new Provider(provider); - this.store.dispatch(new AddDraftRequest(val)); - } +export class NewProviderComponent { + constructor() {} } /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts b/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts index 7a5f48eae..a2fb97fae 100644 --- a/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts +++ b/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts @@ -1,10 +1,14 @@ import { Component, ViewChild } from '@angular/core'; import { TestBed, ComponentFixture, async } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; + import { UploadProviderComponent } from './upload-provider.component'; import { FileService } from '../../core/service/file.service'; import { FileServiceStub } from '../../../testing/file.service.stub'; -import { Observable } from 'rxjs/Observable'; +import * as fromProvider from '../reducer'; +import * as fromCollections from '../../domain/reducer'; @Component({ template: ` { ], imports: [ ReactiveFormsModule, + StoreModule.forRoot({ + collections: combineReducers(fromCollections.reducers), + provider: combineReducers(fromProvider.reducers) + }), ], declarations: [ UploadProviderComponent, diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.ts b/ui/src/app/metadata-provider/container/upload-provider.component.ts index e929e15c9..b90ef8018 100644 --- a/ui/src/app/metadata-provider/container/upload-provider.component.ts +++ b/ui/src/app/metadata-provider/container/upload-provider.component.ts @@ -1,14 +1,12 @@ import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/take'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; import { EntityValidators } from '../../domain/service/entity-validators.service'; import { FileService } from '../../core/service/file.service'; +import * as fromCollections from '../../domain/reducer'; +import { UploadProviderRequest, CreateProviderFromUrlRequest } from '../../domain/action/provider-collection.action'; @Component({ selector: 'upload-provider-form', @@ -27,7 +25,8 @@ export class UploadProviderComponent implements OnInit, OnDestroy { constructor( private fb: FormBuilder, - private fileService: FileService + private fileService: FileService, + private store: Store ) {} ngOnInit(): void { @@ -66,18 +65,18 @@ export class UploadProviderComponent implements OnInit, OnDestroy { saveFromFile(file: File, name: string): void { this.fileService.readAsText(file).subscribe(txt => { - this.upload.emit({ + this.store.dispatch(new UploadProviderRequest({ name: name, - body: txt - }); + body: txt as string + })); }); } saveFromUrl(values: {serviceProviderName: string, url: string}): void { - this.fromUrl.emit({ + this.store.dispatch(new CreateProviderFromUrlRequest({ name: values.serviceProviderName, url: values.url - }); + })); } fileChange($event): void { diff --git a/ui/src/app/metadata-provider/effect/copy.effect.ts b/ui/src/app/metadata-provider/effect/copy.effect.ts new file mode 100644 index 000000000..222715da4 --- /dev/null +++ b/ui/src/app/metadata-provider/effect/copy.effect.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs/observable/of'; +import { switchMap, map, withLatestFrom } from 'rxjs/operators'; + +import * as fromProvider from '../reducer'; +import * as fromCollection from '../../domain/reducer'; + +import { + CopySourceActionUnion, + CopySourceActionTypes, + CreateProviderCopyRequest, + CreateProviderCopySuccess, + CreateProviderCopyError +} from '../action/copy.action'; +import { Provider } from '../../domain/entity/provider'; + + +@Injectable() +export class CopyProviderEffects { + + @Effect() + copyRequest$ = this.actions$.pipe( + ofType(CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST), + map(action => action.payload), + withLatestFrom(this.store.select(fromCollection.getProviderCollection)), + switchMap(([attrs, providers]) => { + const { serviceProviderName, entityId } = attrs; + const provider = providers.find(p => p.entityId === attrs.target); + const { attributeRelease, relyingPartyOverrides } = provider; + const action = provider ? + new CreateProviderCopySuccess(new Provider({ + serviceProviderName, + entityId, + attributeRelease, + relyingPartyOverrides + })) : + new CreateProviderCopyError(new Error('Not found')); + return of(action); + })); + + @Effect({ dispatch: false }) + copyOnCreation$ = this.actions$.pipe( + ofType(CopySourceActionTypes.CREATE_PROVIDER_COPY_SUCCESS), + switchMap(() => this.router.navigate(['/new/copy/confirm'])) + ); + + constructor( + private actions$: Actions, + private store: Store, + private router: Router + ) { } +} diff --git a/ui/src/app/metadata-provider/effect/search.effect.ts b/ui/src/app/metadata-provider/effect/search.effect.ts new file mode 100644 index 000000000..e1f3d907f --- /dev/null +++ b/ui/src/app/metadata-provider/effect/search.effect.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { of } from 'rxjs/observable/of'; +import { map, switchMap, debounceTime, withLatestFrom } from 'rxjs/operators'; + +import { SearchActionTypes, SearchActionUnion, SearchIdsSuccess } from '../action/search.action'; +import * as fromProvider from '../reducer'; +import * as fromCollection from '../../domain/reducer'; + +@Injectable() +export class SearchIdEffects { + + private dbounce = 500; + + @Effect() + searchEntityIds$ = this.actions$.pipe( + ofType(SearchActionTypes.SEARCH_IDS), + map(action => action.payload), + withLatestFrom(this.store.select(fromCollection.getAllEntityIds)), + map(([ query, ids ]) => { + if (!query) { return []; } + return ids.filter(e => this.matcher(e, query)); + }), + switchMap(entities => of(new SearchIdsSuccess(entities))) + ); + + matcher = (value, query) => value ? value.toLocaleLowerCase().match(query.toLocaleLowerCase()) : false; + + constructor( + private actions$: Actions, + private store: Store + ) { } +} diff --git a/ui/src/app/metadata-provider/guard/copy-isset.guard.ts b/ui/src/app/metadata-provider/guard/copy-isset.guard.ts new file mode 100644 index 000000000..85074cd74 --- /dev/null +++ b/ui/src/app/metadata-provider/guard/copy-isset.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + Router, + ActivatedRouteSnapshot, + RouterStateSnapshot +} from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { map, tap } from 'rxjs/operators'; + +import * as fromProvider from '../reducer'; + +@Injectable() +export class CopyIsSetGuard implements CanActivate { + constructor( + private store: Store, + private router: Router + ) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.store.select(fromProvider.getCopy).pipe( + map(copy => !!copy), + tap(isDefined => !isDefined ? this.router.navigate(['/new/copy']) : isDefined) + ); + } +} diff --git a/ui/src/app/metadata-provider/metadata-provider.module.ts b/ui/src/app/metadata-provider/metadata-provider.module.ts index 13f27a75a..e00d45d1f 100644 --- a/ui/src/app/metadata-provider/metadata-provider.module.ts +++ b/ui/src/app/metadata-provider/metadata-provider.module.ts @@ -1,5 +1,5 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; @@ -12,17 +12,26 @@ import { ProviderEditorFormModule } from './component'; import { PrettyXml } from './pipe/pretty-xml.pipe'; import { UploadProviderComponent } from './container/upload-provider.component'; import { BlankProviderComponent } from './container/blank-provider.component'; - +import { CopyProviderComponent } from './container/copy-provider.component'; +import { SharedModule } from '../shared/shared.module'; +import { SearchIdEffects } from './effect/search.effect'; +import * as fromProvider from './reducer'; +import { ConfirmCopyComponent } from './container/confirm-copy.component'; +import { CopyIsSetGuard } from './guard/copy-isset.guard'; +import { CopyProviderEffects } from './effect/copy.effect'; @NgModule({ declarations: [ NewProviderComponent, UploadProviderComponent, BlankProviderComponent, - PrettyXml, + CopyProviderComponent, + ConfirmCopyComponent, + PrettyXml ], entryComponents: [], imports: [ + SharedModule, HttpClientModule, CommonModule, RouterModule, @@ -39,17 +48,53 @@ export class MetadataProviderModule { static forRoot(): ModuleWithProviders { return { ngModule: RootProviderModule, - providers: [] + providers: [ + CopyIsSetGuard + ] }; } } +export const routes: Routes = [ + { + path: 'new', + component: NewProviderComponent, + canActivate: [], + children: [ + { path: '', redirectTo: 'blank', pathMatch: 'prefix' }, + { + path: 'blank', + component: BlankProviderComponent, + canDeactivate: [] + }, + { + path: 'upload', + component: UploadProviderComponent, + canDeactivate: [] + }, + { + path: 'copy', + component: CopyProviderComponent, + canDeactivate: [] + } + ] + }, + { + path: 'new/copy/confirm', + component: ConfirmCopyComponent, + canActivate: [CopyIsSetGuard] + } +]; + @NgModule({ imports: [ MetadataProviderModule, - RouterModule.forChild([ - { path: 'new', component: NewProviderComponent } - ]), + RouterModule.forChild(routes), + StoreModule.forFeature('provider', fromProvider.reducers), + EffectsModule.forFeature([ + SearchIdEffects, + CopyProviderEffects + ]) ], }) export class RootProviderModule { } diff --git a/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts b/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts new file mode 100644 index 000000000..e02339f84 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts @@ -0,0 +1,148 @@ +import { reducer } from './copy.reducer'; +import * as fromProviderCopy from './copy.reducer'; +import * as actions from '../action/copy.action'; +import * as fromCollection from '../../domain/action/provider-collection.action'; +import { CopySourceActionTypes, CopySourceActionUnion, CreateProviderCopyRequest } from '../action/copy.action'; +import { Provider } from '../../domain/entity/provider'; + +const snapshot: fromProviderCopy.CopyState = { ...fromProviderCopy.initialState }; + +describe('Provider -> Copy Reducer', () => { + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(snapshot, {} as any); + expect(result).toEqual(snapshot); + }); + }); + + describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST} action`, () => { + it('should set properties on the state', () => { + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: null, + saving: false + }; + const result = reducer(snapshot, new CreateProviderCopyRequest(obj)); + + expect(result).toEqual(obj); + }); + }); + + describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_SUCCESS} action`, () => { + it('should set properties on the state', () => { + const p = new Provider({}); + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: null, + saving: false + }; + const result = reducer(snapshot, new actions.CreateProviderCopySuccess(p)); + + expect(result.provider).toBe(p); + }); + }); + + describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_ERROR} action`, () => { + it('should set properties on the state', () => { + const p = new Provider({}); + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: null, + saving: false + }; + const result = reducer(snapshot, new actions.CreateProviderCopyError(new Error())); + + expect(result.provider).toBeNull(); + }); + }); + + describe(`${CopySourceActionTypes.UPDATE_PROVIDER_COPY} action`, () => { + it('should set properties on the state', () => { + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: new Provider({}), + saving: false + }; + const result = reducer(snapshot, new actions.UpdateProviderCopy({id: 'foo'})); + + expect(result.provider.id).toBe('foo'); + }); + }); + + describe(`${ fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER } action`, () => { + it('should set properties on the state', () => { + const p = new Provider({}); + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: p, + saving: false + }; + const result = reducer(snapshot, new fromCollection.AddProviderRequest(p)); + + expect(result.saving).toBe(true); + }); + }); + + describe(`${fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS} action`, () => { + it('should set properties on the state', () => { + const p = new Provider({}); + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: p, + saving: false + }; + const result = reducer(snapshot, new fromCollection.AddProviderSuccess(p)); + + expect(result.saving).toBe(false); + }); + }); + + describe(`${fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER_FAIL} action`, () => { + it('should set properties on the state', () => { + const p = new Provider({}); + const obj = { + target: null, + serviceProviderName: null, + entityId: null, + provider: p, + saving: false + }; + const result = reducer(snapshot, new fromCollection.AddProviderFail(p)); + + expect(result.saving).toBe(false); + }); + }); + + describe(`getCopy selector function`, () => { + it('should return the entire copy object', () => { + expect(fromProviderCopy.getCopy(snapshot)).toBe(snapshot.provider); + }); + }); + describe(`getEntityId selector function`, () => { + it('should return the entityId property', () => { + expect(fromProviderCopy.getEntityId(snapshot)).toBe(snapshot.entityId); + }); + }); + describe(`getName selector function`, () => { + it('should return the serviceProviderName property', () => { + expect(fromProviderCopy.getName(snapshot)).toBe(snapshot.serviceProviderName); + }); + }); + describe(`getTarget selector function`, () => { + it('should return the target property', () => { + expect(fromProviderCopy.getTarget(snapshot)).toBe(snapshot.target); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/reducer/copy.reducer.ts b/ui/src/app/metadata-provider/reducer/copy.reducer.ts new file mode 100644 index 000000000..88ce7aaf4 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/copy.reducer.ts @@ -0,0 +1,73 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { CopySourceActionTypes, CopySourceActionUnion } from '../action/copy.action'; +import { MetadataFilter, MetadataProvider } from '../../domain/domain.type'; +import { CopyProviderComponent } from '../container/copy-provider.component'; +import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes } from '../../domain/action/provider-collection.action'; + +export interface CopyState { + target: string; + serviceProviderName: string; + entityId: string; + provider: MetadataProvider; + saving: boolean; +} + +export const initialState: CopyState = { + target: null, + serviceProviderName: null, + entityId: null, + provider: null, + saving: false +}; + +export function reducer(state = initialState, action: CopySourceActionUnion | ProviderCollectionActionsUnion): CopyState { + switch (action.type) { + case CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST: { + return { + ...state, + ...action.payload + }; + } + case CopySourceActionTypes.CREATE_PROVIDER_COPY_SUCCESS: { + return { + ...state, + provider: action.payload + }; + } + case CopySourceActionTypes.UPDATE_PROVIDER_COPY: { + return { + ...state, + provider: { + ...state.provider, + ...action.payload + } + }; + } + case ProviderCollectionActionTypes.ADD_PROVIDER: { + return { + ...state, + saving: true + }; + } + case ProviderCollectionActionTypes.ADD_PROVIDER_FAIL: + case ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS: { + return { + ...initialState + }; + } + default: { + return state; + } + } +} + +export const getTarget = (state: CopyState) => state.target; +export const getName = (state: CopyState) => state.serviceProviderName; +export const getEntityId = (state: CopyState) => state.entityId; +export const getCopy = (state: CopyState) => state.provider; +export const getCopyAttributes = (state: CopyState) => ({ + entityId: state.entityId, + serviceProviderName: state.serviceProviderName, + target: state.target +}); +export const getSaving = (state: CopyState) => state.saving; diff --git a/ui/src/app/metadata-provider/reducer/index.ts b/ui/src/app/metadata-provider/reducer/index.ts new file mode 100644 index 000000000..7e6ec4d65 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/index.ts @@ -0,0 +1,33 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromRoot from '../../app.reducer'; +import * as fromSearch from './search.reducer'; +import * as fromCopy from './copy.reducer'; + +export interface ProviderState { + copy: fromCopy.CopyState; + search: fromSearch.SearchState; +} + +export const reducers = { + copy: fromCopy.reducer, + search: fromSearch.reducer +}; + +export interface State extends fromRoot.State { + 'provider': ProviderState; +} + +export const getProviderState = createFeatureSelector('provider'); + +export const getCopyFromStateFn = (state: ProviderState) => state.copy; +export const getSearchFromStateFn = (state: ProviderState) => state.search; + +export const getCopyFromState = createSelector(getProviderState, getCopyFromStateFn); +export const getCopy = createSelector(getCopyFromState, fromCopy.getCopy); +export const getSaving = createSelector(getCopyFromState, fromCopy.getSaving); +export const getAttributes = createSelector(getCopyFromState, fromCopy.getCopyAttributes); + +export const getSearchFromState = createSelector(getProviderState, getSearchFromStateFn); +export const getSearchResults = createSelector(getSearchFromState, fromSearch.getMatches); +export const getSearchQuery = createSelector(getSearchFromState, fromSearch.getQuery); +export const getSearchLoading = createSelector(getSearchFromState, fromSearch.getSearching); diff --git a/ui/src/app/metadata-provider/reducer/search.reducer.spec.ts b/ui/src/app/metadata-provider/reducer/search.reducer.spec.ts new file mode 100644 index 000000000..a1155b2b5 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/search.reducer.spec.ts @@ -0,0 +1,72 @@ +import { reducer } from './search.reducer'; +import * as fromProviderSearch from './search.reducer'; +import { SearchActionTypes, SearchActionUnion, SearchIds, SearchIdsSuccess, SearchIdsError } from '../action/search.action'; + +const snapshot: fromProviderSearch.SearchState = { + matches: [], + query: '', + searching: false +}; + +describe('Provider -> Search Reducer', () => { + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(snapshot, {} as any); + expect(result).toEqual(snapshot); + }); + }); + + describe(`${SearchActionTypes.SEARCH_IDS} action`, () => { + it('should set properties on the state', () => { + const query = 'foo'; + const result = reducer(snapshot, new SearchIds(query)); + + expect(result).toEqual({ + ...snapshot, + query, + searching: true + }); + }); + }); + + describe(`${SearchActionTypes.SEARCH_IDS_SUCCESS} action`, () => { + it('should set properties on the state', () => { + const matches = ['foo', 'bar', 'baz']; + const result = reducer(snapshot, new SearchIdsSuccess(matches)); + + expect(result).toEqual({ + ...snapshot, + matches, + searching: false + }); + }); + }); + + describe(`${SearchActionTypes.SEARCH_IDS_ERROR} action`, () => { + it('should set properties on the state', () => { + const result = reducer(snapshot, new SearchIdsError(new Error())); + + expect(result).toEqual({ + ...snapshot, + matches: [], + searching: false + }); + }); + }); + + describe(`getQuery selector function`, () => { + it('should return the query property', () => { + expect(fromProviderSearch.getQuery(snapshot)).toBe(snapshot.query); + }); + }); + describe(`getMatches selector function`, () => { + it('should return the matches property', () => { + expect(fromProviderSearch.getMatches(snapshot)).toBe(snapshot.matches); + }); + }); + describe(`getSearching selector function`, () => { + it('should return the searching property', () => { + expect(fromProviderSearch.getSearching(snapshot)).toBe(snapshot.searching); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/reducer/search.reducer.ts b/ui/src/app/metadata-provider/reducer/search.reducer.ts new file mode 100644 index 000000000..734057433 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/search.reducer.ts @@ -0,0 +1,47 @@ +import { SearchActionTypes, SearchActionUnion, SearchIds, SearchIdsSuccess, SearchIdsError } from '../action/search.action'; +import { MetadataProvider } from '../../domain/domain.type'; + +export interface SearchState { + query: string; + matches: string[]; + searching: boolean; +} + +export const initialState: SearchState = { + query: '', + matches: [], + searching: false +}; + +export function reducer(state = initialState, action: SearchActionUnion): SearchState { + switch (action.type) { + case SearchActionTypes.SEARCH_IDS: { + return { + ...state, + query: action.payload, + searching: true + }; + } + case SearchActionTypes.SEARCH_IDS_SUCCESS: { + return { + ...state, + searching: false, + matches: action.payload + }; + } + case SearchActionTypes.SEARCH_IDS_ERROR: { + return { + ...state, + searching: false, + matches: [] + }; + } + default: { + return state; + } + } +} + +export const getQuery = (state: SearchState) => state.query; +export const getMatches = (state: SearchState) => state.matches; +export const getSearching = (state: SearchState) => state.searching; diff --git a/ui/src/locale/en.xlf b/ui/src/locale/en.xlf index 21ced3e8e..983c25677 100644 --- a/ui/src/locale/en.xlf +++ b/ui/src/locale/en.xlf @@ -2175,6 +2175,82 @@ 124 + + Copy + Copy + + app/metadata-provider/container/new-provider.component.ts + 51 + + + + FINISH AND VALIDATE + FINISH AND VALIDATE + + app/metadata-provider/container/copy-provider.component.ts + 13 + + + + Select the Entity ID to copy + Select the Entity ID to copy + + app/metadata-provider/container/copy-provider.component.ts + 25 + + + app/metadata-provider/container/copy-provider.component.ts + 34 + + + + Metadata Source Name (Dashboard Display Only) + Metadata Source Name (Dashboard Display Only) + + app/metadata-provider/container/copy-provider.component.ts + 45 + + + + Add a new metadata source - Finish Summary + Add a new metadata source - Finish Summary + + app/metadata-provider/container/confirm-copy.component.ts + 6 + + + + Edit Filter - + Edit Filter - + + app/metadata-provider/container/edit-filter.component.ts + 8 + + + + Minimum 4 characters. + Minimum 4 characters. + + app/metadata-filter/container/edit-filter.component.ts + 8 + + + + Create + Create + + app/metadata-provider/container/new-provider.component.ts + 8 + + + + Filter Name is required + Filter Name is required + + app/metadata-filter/container/edit-filter.component.ts + 8 + +