From 142cca675416fbfe80f525f2632e0c3eef8d1af2 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Mon, 26 Mar 2018 20:22:36 +0000 Subject: [PATCH] Merged in feature/SHIBUI-297 (pull request #28) SHIBUI-297: Implemented modal search Approved-by: Ryan Mathis --- ui/src/app/core/model/query.ts | 5 ++ .../action/filter.action.spec.ts | 5 +- .../metadata-filter/action/filter.action.ts | 15 ++---- .../component/search-dialog.component.html | 35 +++++++++++-- .../component/search-dialog.component.scss | 6 +++ .../component/search-dialog.component.ts | 50 ++++++++++++++++--- .../container/new-filter.component.html | 10 ++-- .../container/new-filter.component.spec.ts | 2 +- .../container/new-filter.component.ts | 23 +++++---- .../metadata-filter/effect/filter.effect.ts | 35 ++++++++++--- .../reducer/filter.reducer.spec.ts | 9 ++-- .../metadata-filter/reducer/filter.reducer.ts | 15 ++++-- ui/src/app/metadata-filter/reducer/index.ts | 1 + .../service/entity-id.service.ts | 20 ++++---- .../autocomplete/autocomplete.component.ts | 10 ++-- ui/src/locale/en.xlf | 16 ++++++ ui/src/locale/es.xlf | 16 ++++++ 17 files changed, 196 insertions(+), 77 deletions(-) create mode 100644 ui/src/app/core/model/query.ts create mode 100644 ui/src/app/metadata-filter/component/search-dialog.component.scss diff --git a/ui/src/app/core/model/query.ts b/ui/src/app/core/model/query.ts new file mode 100644 index 000000000..5c2cb11a3 --- /dev/null +++ b/ui/src/app/core/model/query.ts @@ -0,0 +1,5 @@ +export interface QueryParams { + term: string; + limit?: number; + offset?: number; +} diff --git a/ui/src/app/metadata-filter/action/filter.action.spec.ts b/ui/src/app/metadata-filter/action/filter.action.spec.ts index f9bf322c1..45d2cda30 100644 --- a/ui/src/app/metadata-filter/action/filter.action.spec.ts +++ b/ui/src/app/metadata-filter/action/filter.action.spec.ts @@ -4,11 +4,10 @@ describe('Filter Actions', () => { it('should provide actions', () => { expect(new actions.CancelCreateFilter().type).toBe(actions.CANCEL_CREATE_FILTER); expect(new actions.CancelViewMore().type).toBe(actions.CANCEL_VIEW_MORE); - expect(new actions.LoadEntityIds('foo').type).toBe(actions.LOAD_ENTITY_IDS); + expect(new actions.QueryEntityIds({term: 'foo'}).type).toBe(actions.QUERY_ENTITY_IDS); expect(new actions.LoadEntityIdsSuccess([]).type).toBe(actions.LOAD_ENTITY_IDS_SUCCESS); expect(new actions.LoadEntityIdsError(new Error('Foobar!')).type).toBe(actions.LOAD_ENTITY_IDS_ERROR); - expect(new actions.ViewMoreIds([]).type).toBe(actions.VIEW_MORE_IDS); + expect(new actions.ViewMoreIds('foo').type).toBe(actions.VIEW_MORE_IDS); expect(new actions.SelectId('foo').type).toBe(actions.SELECT_ID); - expect(new actions.QueryEntityIds([]).type).toBe(actions.QUERY_ENTITY_IDS); }); }); diff --git a/ui/src/app/metadata-filter/action/filter.action.ts b/ui/src/app/metadata-filter/action/filter.action.ts index 30ca61070..15791c2c4 100644 --- a/ui/src/app/metadata-filter/action/filter.action.ts +++ b/ui/src/app/metadata-filter/action/filter.action.ts @@ -1,19 +1,19 @@ import { Action } from '@ngrx/store'; +import { QueryParams } from '../../core/model/query'; + export const QUERY_ENTITY_IDS = '[Filter] Query Entity Ids'; export const VIEW_MORE_IDS = '[Filter] View More Ids Modal'; export const CANCEL_VIEW_MORE = '[Filter] Cancel View More'; export const SELECT_ID = '[Filter] Select Entity ID'; export const CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter'; - -export const LOAD_ENTITY_IDS = '[Entity ID Collection] Load Entity Ids'; export const LOAD_ENTITY_IDS_SUCCESS = '[Entity ID Collection] Load Entity Ids Success'; export const LOAD_ENTITY_IDS_ERROR = '[Entity ID Collection] Load Entity Ids Error'; export class QueryEntityIds implements Action { readonly type = QUERY_ENTITY_IDS; - constructor(public payload: string[]) { } + constructor(public payload: QueryParams) { } } export class CancelCreateFilter implements Action { @@ -23,7 +23,7 @@ export class CancelCreateFilter implements Action { export class ViewMoreIds implements Action { readonly type = VIEW_MORE_IDS; - constructor(public payload: string[]) {} + constructor(public payload: string) { } } export class CancelViewMore implements Action { @@ -36,12 +36,6 @@ export class SelectId implements Action { constructor(public payload: string) { } } -export class LoadEntityIds implements Action { - readonly type = LOAD_ENTITY_IDS; - - constructor(public payload: string) { } -} - export class LoadEntityIdsSuccess implements Action { readonly type = LOAD_ENTITY_IDS_SUCCESS; @@ -59,7 +53,6 @@ export type Actions = | CancelViewMore | CancelCreateFilter | SelectId - | LoadEntityIds | LoadEntityIdsSuccess | LoadEntityIdsError | QueryEntityIds; diff --git a/ui/src/app/metadata-filter/component/search-dialog.component.html b/ui/src/app/metadata-filter/component/search-dialog.component.html index 09590236a..2bf2be6b2 100644 --- a/ui/src/app/metadata-filter/component/search-dialog.component.html +++ b/ui/src/app/metadata-filter/component/search-dialog.component.html @@ -1,16 +1,41 @@ \ No newline at end of file + diff --git a/ui/src/app/metadata-filter/component/search-dialog.component.scss b/ui/src/app/metadata-filter/component/search-dialog.component.scss new file mode 100644 index 000000000..543de14e3 --- /dev/null +++ b/ui/src/app/metadata-filter/component/search-dialog.component.scss @@ -0,0 +1,6 @@ +:host { + .scrolled { + max-height: 400px; + overflow: scroll; + } +} \ No newline at end of file diff --git a/ui/src/app/metadata-filter/component/search-dialog.component.ts b/ui/src/app/metadata-filter/component/search-dialog.component.ts index 5c8e5c7d4..1059a7383 100644 --- a/ui/src/app/metadata-filter/component/search-dialog.component.ts +++ b/ui/src/app/metadata-filter/component/search-dialog.component.ts @@ -1,27 +1,63 @@ -import { Component, OnChanges, OnInit } from '@angular/core'; +import { Component, AfterViewInit, Input, OnInit, SimpleChange, SimpleChanges } from '@angular/core'; import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Observable } from 'rxjs/Observable'; import { Store } from '@ngrx/store'; import * as fromFilter from '../reducer'; +import { QueryEntityIds } from '../action/filter.action'; +import { FormBuilder, FormGroup } from '@angular/forms'; @Component({ selector: 'search-dialog', - templateUrl: './search-dialog.component.html' + templateUrl: './search-dialog.component.html', + styleUrls: ['./search-dialog.component.scss'] }) -export class SearchDialogComponent implements OnInit, OnChanges { +export class SearchDialogComponent implements OnInit, AfterViewInit { + + @Input() term: string; + @Input() source = 'InCommon'; + + matches$: Observable; + + selected: string; + + limit = 100; + dbounce = 500; + + form: FormGroup = this.fb.group({ + search: [''], + entityId: [''] + }); + constructor( public activeModal: NgbActiveModal, - private store: Store + private store: Store, + private fb: FormBuilder ) { - // console.log(activeModal); + // this.query$ = this.store.select(fromFilter.getQuery); + this.matches$ = this.store.select(fromFilter.getEntityCollection); } - ngOnChanges(): void { + ngOnInit(): void { + let search = this.form.get('search'); + search.setValue(this.term); + search.valueChanges + .debounceTime(this.dbounce) + .subscribe(val => + this.store.dispatch( + new QueryEntityIds({ term: val, limit: this.limit }) + ) + ); } - ngOnInit(): void { + ngAfterViewInit(): void { + const { term, limit } = this; + this.store.dispatch(new QueryEntityIds({ term, limit })); + } + select($event: MouseEvent, id: string): void { + $event.preventDefault(); + this.selected = id; } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata-filter/container/new-filter.component.html b/ui/src/app/metadata-filter/container/new-filter.component.html index 925f9749c..999fcf63a 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.html +++ b/ui/src/app/metadata-filter/container/new-filter.component.html @@ -49,7 +49,7 @@ Search Entity ID - + @@ -67,9 +67,9 @@ [matches]="entityIds$ | async" [required]="true" limit="10" - [processing]="loading$ | async" + [processing]="(processing$ | async)" (onChange)="searchEntityIds($event)" - (more)="onViewMore()"> + (more)="onViewMore($event)"> @@ -100,10 +100,6 @@

TESTING:

Form Errors:
{{ form.errors | json }}
-
- Test Options: -
{{ entityIds$ | async | json }}
-
Current Form Values:
{{ form.value | json }}
diff --git a/ui/src/app/metadata-filter/container/new-filter.component.spec.ts b/ui/src/app/metadata-filter/container/new-filter.component.spec.ts index d13e0682f..2c0653760 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.spec.ts +++ b/ui/src/app/metadata-filter/container/new-filter.component.spec.ts @@ -71,7 +71,7 @@ describe('New Metadata Filter Page', () => { describe('onViewMore method', () => { it('should dispatch a viewMoreEntityIds action', () => { fixture.detectChanges(); - instance.onViewMore(); + instance.onViewMore('foo'); expect(store.dispatch).toHaveBeenCalled(); }); }); diff --git a/ui/src/app/metadata-filter/container/new-filter.component.ts b/ui/src/app/metadata-filter/container/new-filter.component.ts index 37640a82c..e3756855c 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.ts +++ b/ui/src/app/metadata-filter/container/new-filter.component.ts @@ -3,13 +3,14 @@ import { FormBuilder, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/takeWhile'; import * as fromFilter from '../reducer'; import { ProviderFormFragmentComponent } from '../../metadata-provider/component/forms/provider-form-fragment.component'; import { ProviderStatusEmitter, ProviderValueEmitter } from '../../metadata-provider/service/provider-change-emitter.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { SearchDialogComponent } from '../component/search-dialog.component'; -import { ViewMoreIds, CancelCreateFilter, LoadEntityIds } from '../action/filter.action'; +import { ViewMoreIds, CancelCreateFilter, QueryEntityIds } from '../action/filter.action'; import { EntityValidators } from '../../metadata-provider/service/entity-validators.service'; @@ -19,12 +20,12 @@ import { EntityValidators } from '../../metadata-provider/service/entity-validat }) export class NewFilterComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { - entityIds$: Observable; ids: string[]; - + entityIds$: Observable; showMore$: Observable; selected$: Observable; loading$: Observable; + processing$: Observable; constructor( private store: Store, @@ -34,12 +35,11 @@ export class NewFilterComponent extends ProviderFormFragmentComponent implements ) { super(fb, statusEmitter, valueEmitter); - // this.form.statusChanges.subscribe(status => console.log(status)); - this.showMore$ = this.store.select(fromFilter.getViewingMore); this.selected$ = this.store.select(fromFilter.getSelected); this.entityIds$ = this.store.select(fromFilter.getEntityCollection); this.loading$ = this.store.select(fromFilter.getIsLoading); + this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); this.entityIds$.subscribe(ids => this.ids = ids); @@ -67,14 +67,17 @@ export class NewFilterComponent extends ProviderFormFragmentComponent implements ngOnDestroy(): void {} - searchEntityIds(query: string): void { - if (query.length >= 4) { - this.store.dispatch(new LoadEntityIds(query)); + searchEntityIds(term: string): void { + if (term.length >= 4 && this.ids.indexOf(term) < 0) { + this.store.dispatch(new QueryEntityIds({ + term, + limit: 10 + })); } } - onViewMore(): void { - this.store.dispatch(new ViewMoreIds(this.ids)); + onViewMore(query: string): void { + this.store.dispatch(new ViewMoreIds(query)); } save(): void { diff --git a/ui/src/app/metadata-filter/effect/filter.effect.ts b/ui/src/app/metadata-filter/effect/filter.effect.ts index 766da16d6..5c5431dee 100644 --- a/ui/src/app/metadata-filter/effect/filter.effect.ts +++ b/ui/src/app/metadata-filter/effect/filter.effect.ts @@ -1,16 +1,19 @@ import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; +import { Router } from '@angular/router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import 'rxjs/add/observable/fromPromise'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/withLatestFrom'; import * as filter from '../action/filter.action'; +import * as fromFilter from '../reducer'; -import { Router } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { SearchDialogComponent } from '../component/search-dialog.component'; import { EntityIdService } from '../../metadata-provider/service/entity-id.service'; @@ -21,7 +24,7 @@ export class FilterEffects { @Effect() loadEntityIds$ = this.actions$ - .ofType(filter.LOAD_ENTITY_IDS) + .ofType(filter.QUERY_ENTITY_IDS) .map(action => action.payload) .debounceTime(this.dbounce) .switchMap(query => @@ -40,17 +43,33 @@ export class FilterEffects { viewMore$ = this.actions$ .ofType(filter.VIEW_MORE_IDS) .map(action => action.payload) - .switchMap(() => - Observable - .fromPromise(this.modalService.open(SearchDialogComponent).result) + .switchMap(q => { + const modal = this.modalService.open(SearchDialogComponent) as NgbModalRef; + const res = modal.result; + modal.componentInstance.term = q; + return Observable + .fromPromise(res) .map(id => new filter.SelectId(id)) - .catch(() => Observable.of(new filter.CancelViewMore())) + .catch(() => Observable.of(new filter.CancelViewMore())); + }); + /* + @Effect() + viewMoreQuery$ = this.actions$ + .ofType(filter.VIEW_MORE_IDS) + .map(action => action.payload) + .switchMap(query => + this.idService + .query(query) + .map(ids => new filter.LoadEntityIdsSuccess(ids)) + .catch(error => Observable.of(new filter.LoadEntityIdsError(error))) ); + */ constructor( private actions$: Actions, private router: Router, private modalService: NgbModal, - private idService: EntityIdService + private idService: EntityIdService, + private store: Store ) { } } 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 39372d4d5..54ea59a6a 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts @@ -7,7 +7,8 @@ const snapshot: fromFilter.FilterState = { selected: null, viewMore: false, loading: false, - error: null + error: null, + term: '' }; describe('Filter Reducer', () => { @@ -21,7 +22,7 @@ describe('Filter Reducer', () => { describe(`${ actions.VIEW_MORE_IDS } action`, () => { it('should set viewMore property to true', () => { - const result = reducer(snapshot, new actions.ViewMoreIds([])); + const result = reducer(snapshot, new actions.ViewMoreIds('foo')); expect(result.viewMore).toBe(true); }); @@ -45,9 +46,9 @@ describe('Filter Reducer', () => { }); }); - describe(`${actions.LOAD_ENTITY_IDS} action`, () => { + describe(`${actions.QUERY_ENTITY_IDS} action`, () => { it('should set loading property to true', () => { - const result = reducer(snapshot, new actions.LoadEntityIds('foo')); + const result = reducer(snapshot, new actions.QueryEntityIds({ term: 'foo' })); expect(result.loading).toBe(true); }); diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.ts index b4a64e1ca..ef435a7c7 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.ts @@ -9,6 +9,7 @@ export interface FilterState { loading: boolean; error: Error | null; selected: string | null; + term: string; } export const initialState: FilterState = { @@ -16,7 +17,8 @@ export const initialState: FilterState = { selected: null, viewMore: false, loading: false, - error: null + error: null, + term: '' }; export function reducer(state = initialState, action: filter.Actions): FilterState { @@ -40,10 +42,16 @@ export function reducer(state = initialState, action: filter.Actions): FilterSta viewMore: false }; } - case filter.LOAD_ENTITY_IDS: { + case filter.CANCEL_CREATE_FILTER: { + return { + ...initialState + }; + } + case filter.QUERY_ENTITY_IDS: { return { ...state, - loading: true + loading: true, + term: action.payload.term }; } case filter.LOAD_ENTITY_IDS_SUCCESS: { @@ -72,3 +80,4 @@ export const getSelected = (state: FilterState) => state.selected; export const getEntityIds = (state: FilterState) => state.entityIds; export const getError = (state: FilterState) => state.error; export const getLoading = (state: FilterState) => state.loading; +export const getTerm = (state: FilterState) => state.term; diff --git a/ui/src/app/metadata-filter/reducer/index.ts b/ui/src/app/metadata-filter/reducer/index.ts index 2f9873975..230cb42ce 100644 --- a/ui/src/app/metadata-filter/reducer/index.ts +++ b/ui/src/app/metadata-filter/reducer/index.ts @@ -24,3 +24,4 @@ export const getSelected = createSelector(getFilterFromState, fromFilter.getSele export const getEntityCollection = createSelector(getFilterFromState, fromFilter.getEntityIds); export const getIsLoading = createSelector(getFilterFromState, fromFilter.getLoading); export const getError = createSelector(getFilterFromState, fromFilter.getError); +export const getTerm = createSelector(getFilterFromState, fromFilter.getTerm); diff --git a/ui/src/app/metadata-provider/service/entity-id.service.ts b/ui/src/app/metadata-provider/service/entity-id.service.ts index 4426844e9..5fb3049bf 100644 --- a/ui/src/app/metadata-provider/service/entity-id.service.ts +++ b/ui/src/app/metadata-provider/service/entity-id.service.ts @@ -7,14 +7,15 @@ import { Subject } from 'rxjs/Subject'; import { IDS } from '../../../data/ids.mock'; import { Storage } from '../../shared/storage'; import { environment } from '../../../environments/environment'; +import { QueryParams } from '../../core/model/query'; const MOCK_INTERVAL = 500; @Injectable() export class EntityIdService { - private endpoint = '/data/ids.json'; - private base = ''; + private endpoint = '/EntityIds/search'; + private base = '/api'; private subj: Subject = new Subject(); @@ -22,19 +23,16 @@ export class EntityIdService { private http: HttpClient ) { } - query(search: string = ''): Observable { - setTimeout(() => { - let found = IDS.filter((option: string) => option.toLocaleLowerCase().match(search.toLocaleLowerCase())); - this.subj.next(found); - }, MOCK_INTERVAL); - return this.subj.asObservable(); - /* + query(q: QueryParams): Observable { + let params: HttpParams = new HttpParams(); + Object.keys(q).forEach(key => params = params.set(key, q[key])); + const opts = { params: params }; return this.http - .get(`${this.base}${this.endpoint}s`) + .get(`${this.base}${this.endpoint}`, opts) + .map(resp => resp.entityIds) .catch(err => { console.log('ERROR LOADING IDS:', err); return Observable.of([] as string[]); }); - */ } } diff --git a/ui/src/app/widget/autocomplete/autocomplete.component.ts b/ui/src/app/widget/autocomplete/autocomplete.component.ts index b97cc3561..38bbafa2a 100644 --- a/ui/src/app/widget/autocomplete/autocomplete.component.ts +++ b/ui/src/app/widget/autocomplete/autocomplete.component.ts @@ -155,7 +155,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, AfterViewInit, $event.stopPropagation(); this.handleInputBlur(); this.input.markAsTouched(); - this.more.emit(); + this.more.emit(this.input.value); } handleComponentBlur(newState: any = {}): void { @@ -163,11 +163,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, AfterViewInit, query = this.input.value, change = this.matches && this.matches[selected] ? this.matches[selected] : null; if (!change) { - if (this.allowCustom) { - change = query; - } else { - change = this.queryOption; - } + change = this.allowCustom ? query : this.queryOption; } this.propagateChange(change); this.propagateTouched(null); @@ -222,7 +218,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, AfterViewInit, const optionsAvailable = this.matches.length > 0; const searchForOptions = (!queryEmpty && queryLongEnough); this.state.setState({ - menuOpen: searchForOptions, + menuOpen: searchForOptions && !this.matches.some(m => m === query), selected: searchForOptions ? ((autoselect && optionsAvailable) ? 0 : -1) : null }); if (this.allowCustom) { diff --git a/ui/src/locale/en.xlf b/ui/src/locale/en.xlf index 851a1a5e0..a966da2fe 100644 --- a/ui/src/locale/en.xlf +++ b/ui/src/locale/en.xlf @@ -2051,6 +2051,22 @@ 79 + + Select ID + Select ID + + app/metadata-filter/component/search-dialog.component.ts + 35 + + + + Search Entity Id () + Search Entity Id () + + app/metadata-filter/component/search-dialog.component.ts + 35 + + diff --git a/ui/src/locale/es.xlf b/ui/src/locale/es.xlf index 69b2504ce..6ddf4cb2e 100644 --- a/ui/src/locale/es.xlf +++ b/ui/src/locale/es.xlf @@ -2047,6 +2047,22 @@ 79 + + Select ID + Select ID (es) + + app/metadata-filter/component/search-dialog.component.ts + 35 + + + + Search Entity Id () + Search Entity Id () (es) + + app/metadata-filter/component/search-dialog.component.ts + 35 + + \ No newline at end of file