Skip to content

Commit 26caab1

Browse files
author
Jean-François Morin
committed
Merge branch 'main' of github.com:jeffmorin/dspace-angular
2 parents 92553e0 + ba5f50e commit 26caab1

26 files changed

Lines changed: 391 additions & 35 deletions

config/config.example.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ homePage:
294294
# No. of communities to list per page on the home page
295295
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
296296
pageSize: 5
297+
# Enable or disable the Discover filters on the homepage
298+
showDiscoverFilters: false
297299

298300
# Item Config
299301
item:
@@ -422,3 +424,12 @@ comcolSelectionSort:
422424
# suggestion:
423425
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
424426
# source: "openaire"
427+
428+
429+
# Search settings
430+
search:
431+
# Settings to enable/disable or configure advanced search filters.
432+
advancedFilters:
433+
enabled: false
434+
# List of filters to enable in "Advanced Search" dropdown
435+
filter: [ 'title', 'author', 'subject', 'entityType' ]

src/app/collection-page/collection-page.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@
5858
<ds-themed-loading *ngIf="collectionRD?.isLoading"
5959
message="{{'loading.collection' | translate}}"></ds-themed-loading>
6060
</div>
61-
</div>
61+
</div>

src/app/community-page/community-page.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Bitstream } from '../core/shared/bitstream.model';
77
import { Community } from '../core/shared/community.model';
88
import { fadeInOut } from '../shared/animations/fade';
99
import { hasValue } from '../shared/empty.util';
10-
import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
10+
import { getAllSucceededRemoteDataPayload } from '../core/shared/operators';
1111
import { AuthService } from '../core/auth/auth.service';
1212
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
1313
import { FeatureID } from '../core/data/feature-authorization/feature-id';
Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
<ds-themed-home-news></ds-themed-home-news>
2-
<div class="container">
3-
<ng-container *ngIf="(site$ | async) as site">
4-
<ds-view-tracker [object]="site"></ds-view-tracker>
5-
</ng-container>
6-
<ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
7-
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
8-
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
2+
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'container-fluid' : 'container'">
3+
<div class="row m-5">
4+
<div class="col-sm-3" *ngIf="appConfig.homePage.showDiscoverFilters">
5+
<ds-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
6+
[inPlaceSearch]="false" [showScopeSelector]="false"></ds-configuration-search-page>
7+
</div>
8+
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'col-sm-9' : 'col-sm-12'">
9+
<ng-container *ngIf="(site$ | async) as site">
10+
<ds-view-tracker [object]="site"></ds-view-tracker>
11+
</ng-container>
12+
<ds-themed-search-form [inPlaceSearch]="false"
13+
[searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
14+
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
15+
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
16+
</div>
17+
</div>
918
</div>
1019
<ds-suggestions-popup></ds-suggestions-popup>

src/app/home-page/home-page.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, Inject, OnInit } from '@angular/core';
22
import { map } from 'rxjs/operators';
33
import { ActivatedRoute } from '@angular/router';
44
import { Observable } from 'rxjs';
55
import { Site } from '../core/shared/site.model';
66
import { environment } from '../../environments/environment';
7+
import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface';
78
@Component({
89
selector: 'ds-home-page',
910
styleUrls: ['./home-page.component.scss'],
@@ -14,6 +15,7 @@ export class HomePageComponent implements OnInit {
1415
site$: Observable<Site>;
1516
recentSubmissionspageSize: number;
1617
constructor(
18+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
1719
private route: ActivatedRoute,
1820
) {
1921
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;

src/app/home-page/home-page.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core';
33
import { SharedModule } from '../shared/shared.module';
44
import { HomeNewsComponent } from './home-news/home-news.component';
55
import { HomePageRoutingModule } from './home-page-routing.module';
6-
76
import { HomePageComponent } from './home-page.component';
87
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
98
import { StatisticsModule } from '../statistics/statistics.module';
@@ -13,6 +12,7 @@ import { RecentItemListComponent } from './recent-item-list/recent-item-list.com
1312
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
1413
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
1514
import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
15+
import { SearchModule } from '../shared/search/search.module';
1616
import { NotificationsModule } from '../notifications/notifications.module';
1717

1818
const DECLARATIONS = [
@@ -29,6 +29,7 @@ const DECLARATIONS = [
2929
imports: [
3030
CommonModule,
3131
SharedModule.withEntryComponents(),
32+
SearchModule,
3233
JournalEntitiesModule.withEntryComponents(),
3334
ResearchEntitiesModule.withEntryComponents(),
3435
HomePageRoutingModule,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<div class="facet-filter d-block mb-3 p-3" [ngClass]="{ 'focus': focusBox }" role="region">
2+
<button (click)="toggle()" (focusin)="focusBox = true" (focusout)="focusBox = false" class="filter-name d-flex"
3+
[attr.aria-expanded]="false"
4+
[attr.aria-label]="((collapsedSearch ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.advanced.filters.head') | translate | lowercase)"
5+
[attr.data-test]="'filter-toggle' | dsBrowserOnly">
6+
<span class="h4 d-inline-block text-left mt-auto mb-auto">
7+
{{'search.advanced.filters.head' | translate}}
8+
</span>
9+
<i class="filter-toggle flex-grow-1 fas p-auto" aria-hidden="true" [ngClass]="collapsedSearch ? 'fa-plus' : 'fa-minus'"
10+
[title]="(collapsedSearch ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
11+
</i>
12+
</button>
13+
<div [@slide]="collapsedSearch ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)"
14+
(@slide.done)="finishSlide($event)" class="search-filter-wrapper"
15+
[ngClass]="{ 'closed' : closed, 'notab': notab }">
16+
<form [class]="'ng-invalid'" [formGroup]="advSearchForm" (ngSubmit)="onSubmit(advSearchForm.value)">
17+
<div class="row">
18+
<div class="col-lg-12">
19+
<select
20+
[className]="(filter.invalid) && (filter.dirty || filter.touched) ? 'form-control is-invalid' :'form-control'"
21+
aria-label="filter" name="filter" id="filter" placeholder="select operator"
22+
formControlName="filter" required>
23+
<ng-container *ngFor="let filter of appConfig.search.advancedFilters.filter;">
24+
<option [value]="filter">
25+
{{'search.filters.filter.' + filter + '.text'| translate}}
26+
</option>
27+
</ng-container>
28+
</select>
29+
</div>
30+
<div class="col-lg-12 mt-1">
31+
<select
32+
[className]="(operator.invalid) && (operator.dirty || operator.touched) ? 'form-control is-invalid' :'form-control'"
33+
aria-label="operator" name="operator" id="operator" formControlName="operator" required>
34+
<option value="equals">{{'search.filters.operator.equals.text'| translate}}</option>
35+
<option value="notequals">{{'search.filters.operator.notequals.text'| translate}}</option>
36+
<option value="contains">{{'search.filters.operator.contains.text'| translate}}</option>
37+
<option value="notcontains">{{'search.filters.operator.notcontains.text'| translate}}</option>
38+
</select>
39+
</div>
40+
<div class="col-lg-12 mt-1">
41+
<input type="text" aria-label="textsearch" class="form-control" id="textsearch" name="textsearch"
42+
formControlName="textsearch" #text [placeholder]="('filter.search.text.placeholder' | translate)" required>
43+
</div>
44+
<div class="col-lg-12 mt-1">
45+
<button class="form-control btn w-50 float-right btn-primary" type="submit"
46+
[disabled]="advSearchForm.invalid">{{'advancesearch.form.submit'| translate}}</button>
47+
</div>
48+
</div>
49+
</form>
50+
</div>
51+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import '../search-filters/search-filter/search-filter.component.scss';
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
3+
import { AdvancedSearchComponent } from './advanced-search.component';
4+
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
5+
import { SearchService } from '../../../core/shared/search/search.service';
6+
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
7+
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
8+
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
9+
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
10+
import { APP_CONFIG } from '../../../../config/app-config.interface';
11+
import { environment } from '../../../../environments/environment';
12+
import { RouterStub } from '../../testing/router.stub';
13+
import { Router } from '@angular/router';
14+
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
15+
import { BrowserOnlyMockPipe } from '../../testing/browser-only-mock.pipe';
16+
import { RouterTestingModule } from '@angular/router/testing';
17+
import { TranslateModule } from '@ngx-translate/core';
18+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
19+
describe('AdvancedSearchComponent', () => {
20+
let component: AdvancedSearchComponent;
21+
let fixture: ComponentFixture<AdvancedSearchComponent>;
22+
let builderService: FormBuilderService = getMockFormBuilderService();
23+
let searchService: SearchService;
24+
let router;
25+
const searchServiceStub = {
26+
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
27+
getClearFiltersQueryParams: () => {
28+
},
29+
getSearchLink: () => {
30+
},
31+
getConfigurationSearchConfig: () => { },
32+
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
33+
};
34+
beforeEach(async () => {
35+
await TestBed.configureTestingModule({
36+
declarations: [AdvancedSearchComponent, BrowserOnlyMockPipe],
37+
imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot(), BrowserAnimationsModule, ReactiveFormsModule],
38+
providers: [
39+
FormBuilder,
40+
{ provide: APP_CONFIG, useValue: environment },
41+
{ provide: FormBuilderService, useValue: builderService },
42+
{ provide: Router, useValue: new RouterStub() },
43+
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
44+
{ provide: RemoteDataBuildService, useValue: {} },
45+
{ provide: SearchService, useValue: searchServiceStub },
46+
],
47+
schemas: [NO_ERRORS_SCHEMA]
48+
}).overrideComponent(AdvancedSearchComponent, {
49+
set: { changeDetection: ChangeDetectionStrategy.Default }
50+
}).compileComponents();
51+
});
52+
53+
beforeEach(() => {
54+
fixture = TestBed.createComponent(AdvancedSearchComponent);
55+
component = fixture.componentInstance;
56+
router = TestBed.inject(Router);
57+
fixture.detectChanges();
58+
});
59+
describe('when the getSearchLink method is called', () => {
60+
const data = { filter: 'title', textsearch: 'demo', operator: 'equals' };
61+
it('should call navigate on the router with the right searchlink and parameters when the filter is provided with a valid operator', () => {
62+
component.advSearchForm.get('textsearch').patchValue('1');
63+
component.advSearchForm.get('filter').patchValue('1');
64+
component.advSearchForm.get('operator').patchValue('1');
65+
66+
component.onSubmit(data);
67+
expect(router.navigate).toHaveBeenCalledWith([undefined], {
68+
queryParams: { ['f.' + data.filter]: data.textsearch + ',' + data.operator },
69+
queryParamsHandling: 'merge'
70+
});
71+
72+
});
73+
});
74+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Component, Inject, Input, OnInit } from '@angular/core';
2+
import { Router } from '@angular/router';
3+
import { slide } from '../../animations/slide';
4+
import { FormBuilder } from '@angular/forms';
5+
import { FormControl, FormGroup, Validators } from '@angular/forms';
6+
import { SearchService } from '../../../core/shared/search/search.service';
7+
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
8+
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
9+
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
10+
@Component({
11+
selector: 'ds-advanced-search',
12+
templateUrl: './advanced-search.component.html',
13+
styleUrls: ['./advanced-search.component.scss'],
14+
animations: [slide],
15+
})
16+
/**
17+
* This component represents the part of the search sidebar that contains advanced filters.
18+
*/
19+
export class AdvancedSearchComponent implements OnInit {
20+
/**
21+
* True when the search component should show results on the current page
22+
*/
23+
@Input() inPlaceSearch;
24+
25+
26+
/**
27+
* Link to the search page
28+
*/
29+
notab: boolean;
30+
31+
closed: boolean;
32+
collapsedSearch = false;
33+
focusBox = false;
34+
35+
advSearchForm: FormGroup;
36+
constructor(
37+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
38+
private formBuilder: FormBuilder,
39+
protected searchService: SearchService,
40+
protected router: Router,
41+
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
42+
}
43+
44+
ngOnInit(): void {
45+
46+
this.advSearchForm = this.formBuilder.group({
47+
textsearch: new FormControl('', {
48+
validators: [Validators.required],
49+
}),
50+
filter: new FormControl('title', {
51+
validators: [Validators.required],
52+
}),
53+
operator: new FormControl('equals',
54+
{ validators: [Validators.required], }),
55+
56+
});
57+
this.collapsedSearch = this.isCollapsed();
58+
59+
}
60+
61+
get textsearch() {
62+
return this.advSearchForm.get('textsearch');
63+
}
64+
65+
get filter() {
66+
return this.advSearchForm.get('filter');
67+
}
68+
69+
get operator() {
70+
return this.advSearchForm.get('operator');
71+
}
72+
paramName(filter) {
73+
return 'f.' + filter;
74+
}
75+
onSubmit(data) {
76+
if (this.advSearchForm.valid) {
77+
let queryParams = { [this.paramName(data.filter)]: data.textsearch + ',' + data.operator };
78+
if (!this.inPlaceSearch) {
79+
this.router.navigate([this.searchService.getSearchLink()], { queryParams: queryParams, queryParamsHandling: 'merge' });
80+
} else {
81+
if (!this.router.url.includes('?')) {
82+
this.router.navigateByUrl(this.router.url + '?f.' + data.filter + '=' + data.textsearch + ',' + data.operator);
83+
} else {
84+
this.router.navigateByUrl(this.router.url + '&f.' + data.filter + '=' + data.textsearch + ',' + data.operator);
85+
}
86+
}
87+
88+
this.advSearchForm.reset({ operator: data.operator, filter: data.filter, textsearch: '' });
89+
}
90+
}
91+
startSlide(event: any): void {
92+
if (event.toState === 'collapsed') {
93+
this.closed = true;
94+
}
95+
if (event.fromState === 'collapsed') {
96+
this.notab = false;
97+
}
98+
}
99+
finishSlide(event: any): void {
100+
if (event.fromState === 'collapsed') {
101+
this.closed = false;
102+
}
103+
if (event.toState === 'collapsed') {
104+
this.notab = true;
105+
}
106+
}
107+
toggle() {
108+
this.collapsedSearch = !this.collapsedSearch;
109+
}
110+
private isCollapsed(): boolean {
111+
return !this.collapsedSearch;
112+
}
113+
114+
}
115+

0 commit comments

Comments
 (0)