From 91a419fb0f17bb85501ef66bf25b8fc2c359de2e Mon Sep 17 00:00:00 2001 From: gaurav patel <100828173+GauravD2t@users.noreply.github.com> Date: Tue, 27 Feb 2024 01:41:28 +0530 Subject: [PATCH] Advance search search page (#2608) * Update homepage-config.interface.ts change comment of homepage-config.interface.ts * advance Search add * slove error while unti test * write unit test * Ensures select element has an accessible name * change data pass into url * error resolve * Search.Filters.Applied.F.Title given name as Title * Advanced filters configurable in the User interface (in config.*.yml) * turn on/off and add filters as a list * remove currenturl * resolve * change envierment config and url pass data * set enabled: false ,and remove debugger ,remove searchConfig * expect clauses add * reslove conflict * merge added * remove commented and design change advance search * moving the "add" button to its own col-lg-12 * resolve conflict * reslove error * merge en.json5 file * remove trailing spaces * resolve Conflicts * Fix typo. property is filter not filters --------- Co-authored-by: Tim Donohue --- config/config.example.yml | 9 ++ .../advanced-search.component.html | 51 ++++++++ .../advanced-search.component.scss | 1 + .../advanced-search.component.spec.ts | 74 +++++++++++ .../advanced-search.component.ts | 115 ++++++++++++++++++ .../search-filters.component.html | 2 + .../search-filters.component.spec.ts | 3 + .../search-filters.component.ts | 6 +- src/app/shared/search/search.module.ts | 2 + src/app/shared/search/search.utils.ts | 2 +- src/assets/i18n/en.json5 | 29 ++++- src/config/advance-search-config.interface.ts | 4 + src/config/app-config.interface.ts | 3 +- src/config/default-app-config.ts | 12 +- src/config/search-page-config.interface.ts | 11 ++ src/environments/environment.test.ts | 10 +- 16 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 src/app/shared/search/advanced-search/advanced-search.component.html create mode 100644 src/app/shared/search/advanced-search/advanced-search.component.scss create mode 100644 src/app/shared/search/advanced-search/advanced-search.component.spec.ts create mode 100644 src/app/shared/search/advanced-search/advanced-search.component.ts create mode 100644 src/config/advance-search-config.interface.ts create mode 100644 src/config/search-page-config.interface.ts diff --git a/config/config.example.yml b/config/config.example.yml index 4fbc98fea2c..8b010ba6ea6 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -424,3 +424,12 @@ comcolSelectionSort: # suggestion: # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af # source: "openaire" + + +# Search settings +search: + # Settings to enable/disable or configure advanced search filters. + advancedFilters: + enabled: false + # List of filters to enable in "Advanced Search" dropdown + filter: [ 'title', 'author', 'subject', 'entityType' ] diff --git a/src/app/shared/search/advanced-search/advanced-search.component.html b/src/app/shared/search/advanced-search/advanced-search.component.html new file mode 100644 index 00000000000..e03fb40532c --- /dev/null +++ b/src/app/shared/search/advanced-search/advanced-search.component.html @@ -0,0 +1,51 @@ +
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/app/shared/search/advanced-search/advanced-search.component.scss b/src/app/shared/search/advanced-search/advanced-search.component.scss new file mode 100644 index 00000000000..13ca767b2b4 --- /dev/null +++ b/src/app/shared/search/advanced-search/advanced-search.component.scss @@ -0,0 +1 @@ +@import '../search-filters/search-filter/search-filter.component.scss'; \ No newline at end of file diff --git a/src/app/shared/search/advanced-search/advanced-search.component.spec.ts b/src/app/shared/search/advanced-search/advanced-search.component.spec.ts new file mode 100644 index 00000000000..a6a7f464910 --- /dev/null +++ b/src/app/shared/search/advanced-search/advanced-search.component.spec.ts @@ -0,0 +1,74 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { AdvancedSearchComponent } from './advanced-search.component'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { APP_CONFIG } from '../../../../config/app-config.interface'; +import { environment } from '../../../../environments/environment'; +import { RouterStub } from '../../testing/router.stub'; +import { Router } from '@angular/router'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowserOnlyMockPipe } from '../../testing/browser-only-mock.pipe'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +describe('AdvancedSearchComponent', () => { + let component: AdvancedSearchComponent; + let fixture: ComponentFixture; + let builderService: FormBuilderService = getMockFormBuilderService(); + let searchService: SearchService; + let router; + const searchServiceStub = { + /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ + getClearFiltersQueryParams: () => { + }, + getSearchLink: () => { + }, + getConfigurationSearchConfig: () => { }, + /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + }; + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AdvancedSearchComponent, BrowserOnlyMockPipe], + imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot(), BrowserAnimationsModule, ReactiveFormsModule], + providers: [ + FormBuilder, + { provide: APP_CONFIG, useValue: environment }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: new RouterStub() }, + { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: SearchService, useValue: searchServiceStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(AdvancedSearchComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdvancedSearchComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + fixture.detectChanges(); + }); + describe('when the getSearchLink method is called', () => { + const data = { filter: 'title', textsearch: 'demo', operator: 'equals' }; + it('should call navigate on the router with the right searchlink and parameters when the filter is provided with a valid operator', () => { + component.advSearchForm.get('textsearch').patchValue('1'); + component.advSearchForm.get('filter').patchValue('1'); + component.advSearchForm.get('operator').patchValue('1'); + + component.onSubmit(data); + expect(router.navigate).toHaveBeenCalledWith([undefined], { + queryParams: { ['f.' + data.filter]: data.textsearch + ',' + data.operator }, + queryParamsHandling: 'merge' + }); + + }); + }); +}); diff --git a/src/app/shared/search/advanced-search/advanced-search.component.ts b/src/app/shared/search/advanced-search/advanced-search.component.ts new file mode 100644 index 00000000000..ca6a7c9e28b --- /dev/null +++ b/src/app/shared/search/advanced-search/advanced-search.component.ts @@ -0,0 +1,115 @@ +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { slide } from '../../animations/slide'; +import { FormBuilder } from '@angular/forms'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; +import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; +@Component({ + selector: 'ds-advanced-search', + templateUrl: './advanced-search.component.html', + styleUrls: ['./advanced-search.component.scss'], + animations: [slide], +}) + /** + * This component represents the part of the search sidebar that contains advanced filters. + */ +export class AdvancedSearchComponent implements OnInit { + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch; + + + /** + * Link to the search page + */ + notab: boolean; + + closed: boolean; + collapsedSearch = false; + focusBox = false; + + advSearchForm: FormGroup; + constructor( + @Inject(APP_CONFIG) protected appConfig: AppConfig, + private formBuilder: FormBuilder, + protected searchService: SearchService, + protected router: Router, + @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) { + } + + ngOnInit(): void { + + this.advSearchForm = this.formBuilder.group({ + textsearch: new FormControl('', { + validators: [Validators.required], + }), + filter: new FormControl('title', { + validators: [Validators.required], + }), + operator: new FormControl('equals', + { validators: [Validators.required], }), + + }); + this.collapsedSearch = this.isCollapsed(); + + } + + get textsearch() { + return this.advSearchForm.get('textsearch'); + } + + get filter() { + return this.advSearchForm.get('filter'); + } + + get operator() { + return this.advSearchForm.get('operator'); + } + paramName(filter) { + return 'f.' + filter; + } + onSubmit(data) { + if (this.advSearchForm.valid) { + let queryParams = { [this.paramName(data.filter)]: data.textsearch + ',' + data.operator }; + if (!this.inPlaceSearch) { + this.router.navigate([this.searchService.getSearchLink()], { queryParams: queryParams, queryParamsHandling: 'merge' }); + } else { + if (!this.router.url.includes('?')) { + this.router.navigateByUrl(this.router.url + '?f.' + data.filter + '=' + data.textsearch + ',' + data.operator); + } else { + this.router.navigateByUrl(this.router.url + '&f.' + data.filter + '=' + data.textsearch + ',' + data.operator); + } + } + + this.advSearchForm.reset({ operator: data.operator, filter: data.filter, textsearch: '' }); + } + } + startSlide(event: any): void { + if (event.toState === 'collapsed') { + this.closed = true; + } + if (event.fromState === 'collapsed') { + this.notab = false; + } + } + finishSlide(event: any): void { + if (event.fromState === 'collapsed') { + this.closed = false; + } + if (event.toState === 'collapsed') { + this.notab = true; + } + } + toggle() { + this.collapsedSearch = !this.collapsedSearch; + } + private isCollapsed(): boolean { + return !this.collapsedSearch; + } + +} + diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index b5377f502ba..3f70a759bcb 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -5,4 +5,6 @@

{{filterLabel+'.filters.head' | translate}}

+ {{"search.filters.reset" | translate}} diff --git a/src/app/shared/search/search-filters/search-filters.component.spec.ts b/src/app/shared/search/search-filters/search-filters.component.spec.ts index 85fd8e09a12..246b9fb8b7f 100644 --- a/src/app/shared/search/search-filters/search-filters.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filters.component.spec.ts @@ -9,6 +9,8 @@ import { SearchFiltersComponent } from './search-filters.component'; import { SearchService } from '../../../core/shared/search/search.service'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { APP_CONFIG } from 'src/config/app-config.interface'; +import { environment } from 'src/environments/environment'; describe('SearchFiltersComponent', () => { let comp: SearchFiltersComponent; @@ -38,6 +40,7 @@ describe('SearchFiltersComponent', () => { { provide: SearchService, useValue: searchServiceStub }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SearchFilterService, useValue: searchFiltersStub }, + { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index 57cce617888..51065f827fa 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; - +import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; import { SearchService } from '../../../core/shared/search/search.service'; import { RemoteData } from '../../../core/data/remote-data'; import { SearchFilterConfig } from '../models/search-filter-config.model'; @@ -12,6 +12,7 @@ import { SearchFilterService } from '../../../core/shared/search/search-filter.s import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { currentPath } from '../../utils/route.utils'; import { hasValue } from '../../empty.util'; +import { PaginatedSearchOptions } from '../models/paginated-search-options.model'; @Component({ selector: 'ds-search-filters', @@ -28,7 +29,7 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { * An observable containing configuration about which filters are shown and how they are shown */ @Input() filters: Observable>; - + @Input() searchOptions: PaginatedSearchOptions; /** * List of all filters that are currently active with their value set to null. * Used to reset all filters at once @@ -71,6 +72,7 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { * @param {SearchConfigurationService} searchConfigService */ constructor( + @Inject(APP_CONFIG) protected appConfig: AppConfig, private searchService: SearchService, private filterService: SearchFilterService, private router: Router, diff --git a/src/app/shared/search/search.module.ts b/src/app/shared/search/search.module.ts index d8474a5ea38..efb1a12581e 100644 --- a/src/app/shared/search/search.module.ts +++ b/src/app/shared/search/search.module.ts @@ -32,6 +32,7 @@ import { ThemedSearchComponent } from './themed-search.component'; import { ThemedSearchResultsComponent } from './search-results/themed-search-results.component'; import { ThemedSearchSettingsComponent } from './search-settings/themed-search-settings.component'; import { NouisliderModule } from 'ng2-nouislider'; +import { AdvancedSearchComponent } from './advanced-search/advanced-search.component'; import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component'; import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component'; const COMPONENTS = [ @@ -59,6 +60,7 @@ const COMPONENTS = [ ThemedConfigurationSearchPageComponent, ThemedSearchResultsComponent, ThemedSearchSettingsComponent, + AdvancedSearchComponent, ThemedSearchFiltersComponent, ThemedSearchSidebarComponent, ]; diff --git a/src/app/shared/search/search.utils.ts b/src/app/shared/search/search.utils.ts index cfb96a52856..d5891f2b4e4 100644 --- a/src/app/shared/search/search.utils.ts +++ b/src/app/shared/search/search.utils.ts @@ -49,7 +49,7 @@ export function stripOperatorFromFilterValue(value: string) { * @param operator */ export function addOperatorToFilterValue(value: string, operator: string) { - if (!value.match(new RegExp(`^.+,(equals|query|authority)$`))) { + if (!value.match(new RegExp(`^.+,(equals|query|authority|contains|notcontains|notequals)$`))) { return `${value},${operator}`; } return value; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 57ed7d30671..a133cf5a3e6 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -5699,5 +5699,32 @@ "admin.notifications.publicationclaim.breadcrumbs": "Publication Claim", "admin.notifications.publicationclaim.page.title": "Publication Claim", -} + "filter.search.operator.placeholder": "Operator", + + "search.filters.filter.entityType.text": "Item Type", + + "search.filters.operator.equals.text": "Equals", + + "search.filters.operator.notequals.text": "Not Equals", + + "search.filters.operator.notcontains.text": "Not Contains", + + "search.filters.operator.contains.text": "Contains", + + "search.filters.filter.title.text": "Title", + + "search.filters.applied.f.title": "Title", + + "search.filters.filter.author.text": "Author", + + "search.filters.filter.subject.text": "Subject", + + "search.advanced.filters.head": "Advanced Search", + + "filter.search.operator.placeholder": "Operator", + + "filter.search.text.placeholder": "Search text", + + "advancesearch.form.submit": "Add", +} \ No newline at end of file diff --git a/src/config/advance-search-config.interface.ts b/src/config/advance-search-config.interface.ts new file mode 100644 index 00000000000..092dc008fcf --- /dev/null +++ b/src/config/advance-search-config.interface.ts @@ -0,0 +1,4 @@ +export interface AdvancedSearchConfig { + enabled: boolean; + filter: string[]; +} diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 51a116fa70e..3f432bfcad9 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -25,7 +25,7 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { QualityAssuranceConfig } from './quality-assurance.config'; - +import { SearchConfig } from './search-page-config.interface'; interface AppConfig extends Config { ui: UIServerConfig; rest: ServerConfig; @@ -54,6 +54,7 @@ interface AppConfig extends Config { vocabularies: FilterVocabularyConfig[]; comcolSelectionSort: DiscoverySortConfig; qualityAssuranceConfig: QualityAssuranceConfig; + search: SearchConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index ae5c6c1d00c..9ba5ee9a350 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -24,8 +24,8 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { CommunityPageConfig } from './community-page-config.interface'; -import {QualityAssuranceConfig} from './quality-assurance.config'; - +import { QualityAssuranceConfig } from './quality-assurance.config'; +import { SearchConfig } from './search-page-config.interface'; export class DefaultAppConfig implements AppConfig { production = false; @@ -498,4 +498,12 @@ export class DefaultAppConfig implements AppConfig { }, pageSize: 5, }; + + + search: SearchConfig = { + advancedFilters: { + enabled: false, + filter: ['title', 'author', 'subject', 'entityType'] + } + }; } diff --git a/src/config/search-page-config.interface.ts b/src/config/search-page-config.interface.ts new file mode 100644 index 00000000000..70575c06015 --- /dev/null +++ b/src/config/search-page-config.interface.ts @@ -0,0 +1,11 @@ +import { Config } from './config.interface'; +import { AdvancedSearchConfig } from './advance-search-config.interface'; +export interface SearchConfig extends Config { + + /** + * List of standard filter to select in adding advanced Search + * Used by {@link UploadBitstreamComponent}. + */ + advancedFilters: AdvancedSearchConfig; + +} diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index e0cf1eb2072..6f3ab32fa91 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -334,5 +334,13 @@ export const environment: BuildConfig = { } ], - suggestion: [] + suggestion: [], + + search: { + advancedFilters: { + enabled: false, + filter: ['title', 'author', 'subject', 'entityType'] + } + } + };