Merge remote-tracking branch 'origin/main' into coar-notify-7

This commit is contained in:
frabacche
2024-02-27 09:34:58 +01:00
16 changed files with 327 additions and 10 deletions

View File

@@ -424,3 +424,12 @@ comcolSelectionSort:
# suggestion: # suggestion:
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
# source: "openaire" # 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' ]

View File

@@ -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>

View File

@@ -0,0 +1 @@
@import '../search-filters/search-filter/search-filter.component.scss';

View File

@@ -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'
});
});
});
});

View File

@@ -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;
}
}

View File

@@ -5,4 +5,6 @@
<ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter> <ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
</div> </div>
</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> <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>

View File

@@ -9,6 +9,8 @@ import { SearchFiltersComponent } from './search-filters.component';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; 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', () => { describe('SearchFiltersComponent', () => {
let comp: SearchFiltersComponent; let comp: SearchFiltersComponent;
@@ -38,6 +40,7 @@ describe('SearchFiltersComponent', () => {
{ provide: SearchService, useValue: searchServiceStub }, { provide: SearchService, useValue: searchServiceStub },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: SearchFilterService, useValue: searchFiltersStub }, { provide: SearchFilterService, useValue: searchFiltersStub },
{ provide: APP_CONFIG, useValue: environment },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { SearchFilterConfig } from '../models/search-filter-config.model'; 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 { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
import { currentPath } from '../../utils/route.utils'; import { currentPath } from '../../utils/route.utils';
import { hasValue } from '../../empty.util'; import { hasValue } from '../../empty.util';
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
@Component({ @Component({
selector: 'ds-search-filters', 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 * An observable containing configuration about which filters are shown and how they are shown
*/ */
@Input() filters: Observable<RemoteData<SearchFilterConfig[]>>; @Input() filters: Observable<RemoteData<SearchFilterConfig[]>>;
@Input() searchOptions: PaginatedSearchOptions;
/** /**
* List of all filters that are currently active with their value set to null. * List of all filters that are currently active with their value set to null.
* Used to reset all filters at once * Used to reset all filters at once
@@ -71,6 +72,7 @@ export class SearchFiltersComponent implements OnInit, OnDestroy {
* @param {SearchConfigurationService} searchConfigService * @param {SearchConfigurationService} searchConfigService
*/ */
constructor( constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
private searchService: SearchService, private searchService: SearchService,
private filterService: SearchFilterService, private filterService: SearchFilterService,
private router: Router, private router: Router,

View File

@@ -32,6 +32,7 @@ import { ThemedSearchComponent } from './themed-search.component';
import { ThemedSearchResultsComponent } from './search-results/themed-search-results.component'; import { ThemedSearchResultsComponent } from './search-results/themed-search-results.component';
import { ThemedSearchSettingsComponent } from './search-settings/themed-search-settings.component'; import { ThemedSearchSettingsComponent } from './search-settings/themed-search-settings.component';
import { NouisliderModule } from 'ng2-nouislider'; import { NouisliderModule } from 'ng2-nouislider';
import { AdvancedSearchComponent } from './advanced-search/advanced-search.component';
import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component'; import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component';
import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component'; import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component';
const COMPONENTS = [ const COMPONENTS = [
@@ -59,6 +60,7 @@ const COMPONENTS = [
ThemedConfigurationSearchPageComponent, ThemedConfigurationSearchPageComponent,
ThemedSearchResultsComponent, ThemedSearchResultsComponent,
ThemedSearchSettingsComponent, ThemedSearchSettingsComponent,
AdvancedSearchComponent,
ThemedSearchFiltersComponent, ThemedSearchFiltersComponent,
ThemedSearchSidebarComponent, ThemedSearchSidebarComponent,
]; ];

View File

@@ -49,7 +49,7 @@ export function stripOperatorFromFilterValue(value: string) {
* @param operator * @param operator
*/ */
export function addOperatorToFilterValue(value: string, operator: string) { 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},${operator}`;
} }
return value; return value;

View File

@@ -5698,6 +5698,24 @@
"admin.notifications.publicationclaim.page.title": "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",
"coar-notify-support.title": "COAR Notify Protocol", "coar-notify-support.title": "COAR Notify Protocol",
"coar-notify-support-title.content": "Here, we fully support the COAR Notify protocol, which is designed to enhance the communication between repositories. To learn more about the COAR Notify protocol, visit the <a href=\\\"https://notify.coar-repositories.org/\\\">COAR Notify website</a>.", "coar-notify-support-title.content": "Here, we fully support the COAR Notify protocol, which is designed to enhance the communication between repositories. To learn more about the COAR Notify protocol, visit the <a href=\\\"https://notify.coar-repositories.org/\\\">COAR Notify website</a>.",
@@ -6011,3 +6029,13 @@
"type-equals-journal-article_condition.label": "Type equals Journal Article", "type-equals-journal-article_condition.label": "Type equals Journal Article",
} }
"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",
}

View File

@@ -0,0 +1,4 @@
export interface AdvancedSearchConfig {
enabled: boolean;
filter: string[];
}

View File

@@ -25,7 +25,7 @@ import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { FilterVocabularyConfig } from './filter-vocabulary-config';
import { DiscoverySortConfig } from './discovery-sort.config'; import { DiscoverySortConfig } from './discovery-sort.config';
import { QualityAssuranceConfig } from './quality-assurance.config'; import { QualityAssuranceConfig } from './quality-assurance.config';
import { SearchConfig } from './search-page-config.interface';
interface AppConfig extends Config { interface AppConfig extends Config {
ui: UIServerConfig; ui: UIServerConfig;
rest: ServerConfig; rest: ServerConfig;
@@ -54,6 +54,7 @@ interface AppConfig extends Config {
vocabularies: FilterVocabularyConfig[]; vocabularies: FilterVocabularyConfig[];
comcolSelectionSort: DiscoverySortConfig; comcolSelectionSort: DiscoverySortConfig;
qualityAssuranceConfig: QualityAssuranceConfig; qualityAssuranceConfig: QualityAssuranceConfig;
search: SearchConfig;
} }
/** /**

View File

@@ -24,8 +24,8 @@ import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { FilterVocabularyConfig } from './filter-vocabulary-config';
import { DiscoverySortConfig } from './discovery-sort.config'; import { DiscoverySortConfig } from './discovery-sort.config';
import { CommunityPageConfig } from './community-page-config.interface'; 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 { export class DefaultAppConfig implements AppConfig {
production = false; production = false;
@@ -498,4 +498,12 @@ export class DefaultAppConfig implements AppConfig {
}, },
pageSize: 5, pageSize: 5,
}; };
search: SearchConfig = {
advancedFilters: {
enabled: false,
filter: ['title', 'author', 'subject', 'entityType']
}
};
} }

View File

@@ -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;
}

View File

@@ -275,10 +275,6 @@ export const environment: BuildConfig = {
undoTimeout: 10000 // 10 seconds undoTimeout: 10000 // 10 seconds
} }
}, },
suggestion: [],
themes: [ themes: [
{ {
name: 'full-item-page-theme', name: 'full-item-page-theme',
@@ -337,4 +333,14 @@ export const environment: BuildConfig = {
enabled: true enabled: true
} }
], ],
suggestion: [],
search: {
advancedFilters: {
enabled: false,
filter: ['title', 'author', 'subject', 'entityType']
}
}
}; };