Skip to content

Commit

Permalink
Merge branch 'main' of github.com:jeffmorin/dspace-angular
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-François Morin committed Feb 27, 2024
2 parents 92553e0 + ba5f50e commit 26caab1
Show file tree
Hide file tree
Showing 26 changed files with 391 additions and 35 deletions.
11 changes: 11 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ homePage:
# No. of communities to list per page on the home page
# 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
pageSize: 5
# Enable or disable the Discover filters on the homepage
showDiscoverFilters: false

# Item Config
item:
Expand Down Expand Up @@ -422,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' ]
2 changes: 1 addition & 1 deletion src/app/collection-page/collection-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@
<ds-themed-loading *ngIf="collectionRD?.isLoading"
message="{{'loading.collection' | translate}}"></ds-themed-loading>
</div>
</div>
</div>
2 changes: 1 addition & 1 deletion src/app/community-page/community-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Bitstream } from '../core/shared/bitstream.model';
import { Community } from '../core/shared/community.model';
import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util';
import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
import { getAllSucceededRemoteDataPayload } from '../core/shared/operators';
import { AuthService } from '../core/auth/auth.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
Expand Down
23 changes: 16 additions & 7 deletions src/app/home-page/home-page.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
<ds-themed-home-news></ds-themed-home-news>
<div class="container">
<ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker>
</ng-container>
<ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'container-fluid' : 'container'">
<div class="row m-5">
<div class="col-sm-3" *ngIf="appConfig.homePage.showDiscoverFilters">
<ds-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
[inPlaceSearch]="false" [showScopeSelector]="false"></ds-configuration-search-page>
</div>
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'col-sm-9' : 'col-sm-12'">
<ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker>
</ng-container>
<ds-themed-search-form [inPlaceSearch]="false"
[searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
</div>
</div>
</div>
<ds-suggestions-popup></ds-suggestions-popup>
4 changes: 3 additions & 1 deletion src/app/home-page/home-page.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { map } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { Site } from '../core/shared/site.model';
import { environment } from '../../environments/environment';
import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface';
@Component({
selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'],
Expand All @@ -14,6 +15,7 @@ export class HomePageComponent implements OnInit {
site$: Observable<Site>;
recentSubmissionspageSize: number;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
private route: ActivatedRoute,
) {
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;
Expand Down
3 changes: 2 additions & 1 deletion src/app/home-page/home-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { HomeNewsComponent } from './home-news/home-news.component';
import { HomePageRoutingModule } from './home-page-routing.module';

import { HomePageComponent } from './home-page.component';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
import { StatisticsModule } from '../statistics/statistics.module';
Expand All @@ -13,6 +12,7 @@ import { RecentItemListComponent } from './recent-item-list/recent-item-list.com
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
import { SearchModule } from '../shared/search/search.module';
import { NotificationsModule } from '../notifications/notifications.module';

const DECLARATIONS = [
Expand All @@ -29,6 +29,7 @@ const DECLARATIONS = [
imports: [
CommonModule,
SharedModule.withEntryComponents(),
SearchModule,
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
HomePageRoutingModule,
Expand Down
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 @@ -157,11 +157,20 @@ export class SearchFilterComponent implements OnInit {
}

get regionId(): string {
return `search-filter-region-${this.sequenceId}`;
if (this.inPlaceSearch) {
return `search-filter-region-${this.sequenceId}`;
} else {
return `search-filter-region-home-${this.sequenceId}`;
}

}

get toggleId(): string {
return `search-filter-toggle-${this.sequenceId}`;
if (this.inPlaceSearch) {
return `search-filter-toggle-${this.sequenceId}`;
} else {
return `search-filter-toggle-home-${this.sequenceId}`;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<h3>{{"search.filters.head" | translate}}</h3>
<h3 *ngIf="inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h3>
<h2 *ngIf="!inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h2>
<div *ngIf="(filters | async)?.hasSucceeded">
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
<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>
Loading

0 comments on commit 26caab1

Please sign in to comment.