Skip to content

Commit

Permalink
Advance search search page (DSpace#2608)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
GauravD2t and tdonohue authored Feb 26, 2024
1 parent bfeeeac commit 91a419f
Show file tree
Hide file tree
Showing 16 changed files with 326 additions and 8 deletions.
9 changes: 9 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<div class="facet-filter d-block mb-3 p-3" [ngClass]="{ 'focus': focusBox }" role="region">
<button (click)="toggle()" (focusin)="focusBox = true" (focusout)="focusBox = false" class="filter-name d-flex"
[attr.aria-expanded]="false"
[attr.aria-label]="((collapsedSearch ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.advanced.filters.head') | translate | lowercase)"
[attr.data-test]="'filter-toggle' | dsBrowserOnly">
<span class="h4 d-inline-block text-left mt-auto mb-auto">
{{'search.advanced.filters.head' | translate}}
</span>
<i class="filter-toggle flex-grow-1 fas p-auto" aria-hidden="true" [ngClass]="collapsedSearch ? 'fa-plus' : 'fa-minus'"
[title]="(collapsedSearch ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
</i>
</button>
<div [@slide]="collapsedSearch ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)"
(@slide.done)="finishSlide($event)" class="search-filter-wrapper"
[ngClass]="{ 'closed' : closed, 'notab': notab }">
<form [class]="'ng-invalid'" [formGroup]="advSearchForm" (ngSubmit)="onSubmit(advSearchForm.value)">
<div class="row">
<div class="col-lg-12">
<select
[className]="(filter.invalid) && (filter.dirty || filter.touched) ? 'form-control is-invalid' :'form-control'"
aria-label="filter" name="filter" id="filter" placeholder="select operator"
formControlName="filter" required>
<ng-container *ngFor="let filter of appConfig.search.advancedFilters.filter;">
<option [value]="filter">
{{'search.filters.filter.' + filter + '.text'| translate}}
</option>
</ng-container>
</select>
</div>
<div class="col-lg-12 mt-1">
<select
[className]="(operator.invalid) && (operator.dirty || operator.touched) ? 'form-control is-invalid' :'form-control'"
aria-label="operator" name="operator" id="operator" formControlName="operator" required>
<option value="equals">{{'search.filters.operator.equals.text'| translate}}</option>
<option value="notequals">{{'search.filters.operator.notequals.text'| translate}}</option>
<option value="contains">{{'search.filters.operator.contains.text'| translate}}</option>
<option value="notcontains">{{'search.filters.operator.notcontains.text'| translate}}</option>
</select>
</div>
<div class="col-lg-12 mt-1">
<input type="text" aria-label="textsearch" class="form-control" id="textsearch" name="textsearch"
formControlName="textsearch" #text [placeholder]="('filter.search.text.placeholder' | translate)" required>
</div>
<div class="col-lg-12 mt-1">
<button class="form-control btn w-50 float-right btn-primary" type="submit"
[disabled]="advSearchForm.invalid">{{'advancesearch.form.submit'| translate}}</button>
</div>
</div>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '../search-filters/search-filter/search-filter.component.scss';
Original file line number Diff line number Diff line change
@@ -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<AdvancedSearchComponent>;
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'
});

});
});
});
115 changes: 115 additions & 0 deletions src/app/shared/search/advanced-search/advanced-search.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ <h2 *ngIf="!inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h2>
<ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
</div>
</div>
<ds-advanced-search *ngIf="appConfig.search.advancedFilters.enabled"
[inPlaceSearch]="inPlaceSearch"></ds-advanced-search>
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand All @@ -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<RemoteData<SearchFilterConfig[]>>;

@Input() searchOptions: PaginatedSearchOptions;
/**
* List of all filters that are currently active with their value set to null.
* Used to reset all filters at once
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/search/search.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -59,6 +60,7 @@ const COMPONENTS = [
ThemedConfigurationSearchPageComponent,
ThemedSearchResultsComponent,
ThemedSearchSettingsComponent,
AdvancedSearchComponent,
ThemedSearchFiltersComponent,
ThemedSearchSidebarComponent,
];
Expand Down
2 changes: 1 addition & 1 deletion src/app/shared/search/search.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
4 changes: 4 additions & 0 deletions src/config/advance-search-config.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface AdvancedSearchConfig {
enabled: boolean;
filter: string[];
}
3 changes: 2 additions & 1 deletion src/config/app-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +54,7 @@ interface AppConfig extends Config {
vocabularies: FilterVocabularyConfig[];
comcolSelectionSort: DiscoverySortConfig;
qualityAssuranceConfig: QualityAssuranceConfig;
search: SearchConfig;
}

/**
Expand Down
Loading

0 comments on commit 91a419f

Please sign in to comment.