Merge pull request #2732 from alexandrevryghem/added-recently-added-section-to-community-page_contribute-main

Added search facets to community & collection pages
This commit is contained in:
Tim Donohue
2024-02-21 12:17:09 -06:00
committed by GitHub
33 changed files with 283 additions and 218 deletions

View File

@@ -285,8 +285,17 @@ item:
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
pageSize: 5 pageSize: 5
# Community Page Config
community:
# Search tab config
searchSection:
showSidebar: true
# Collection Page Config # Collection Page Config
collection: collection:
# Search tab config
searchSection:
showSidebar: true
edit: edit:
undoTimeout: 10000 # 10 seconds undoTimeout: 10000 # 10 seconds
@@ -391,4 +400,3 @@ comcolSelectionSort:
# suggestion: # suggestion:
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
# source: "openaire" # source: "openaire"

View File

@@ -25,7 +25,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { BrowseByGuard } from '../browse-by/browse-by-guard';
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -73,7 +73,7 @@ import { CollectionRecentlyAddedComponent } from './sections/recently-added/coll
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: CollectionRecentlyAddedComponent, component: ComcolSearchSectionComponent,
}, },
{ {
path: 'browse/:id', path: 'browse/:id',

View File

@@ -19,7 +19,6 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
import { DsoSharedModule } from '../dso-shared/dso-shared.module'; import { DsoSharedModule } from '../dso-shared/dso-shared.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module';
import { BrowseByPageModule } from '../browse-by/browse-by-page.module'; import { BrowseByPageModule } from '../browse-by/browse-by-page.module';
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component';
const DECLARATIONS = [ const DECLARATIONS = [
CollectionPageComponent, CollectionPageComponent,
@@ -29,7 +28,6 @@ const DECLARATIONS = [
EditItemTemplatePageComponent, EditItemTemplatePageComponent,
ThemedEditItemTemplatePageComponent, ThemedEditItemTemplatePageComponent,
CollectionItemMapperComponent, CollectionItemMapperComponent,
CollectionRecentlyAddedComponent,
]; ];
@NgModule({ @NgModule({

View File

@@ -1,18 +0,0 @@
<ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
<h3 class="sr-only">{{'collection.page.browse.recent.head' | translate}}</h3>
<ds-viewable-collection
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="itemRD"
[hideGear]="true">
</ds-viewable-collection>
</div>
<ds-error *ngIf="itemRD?.hasFailed"
message="{{'error.recent-submissions' | translate}}"></ds-error>
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
{{'collection.page.browse.recent.empty' | translate}}
</div>
</ng-container>

View File

@@ -1,53 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CollectionRecentlyAddedComponent } from './collection-recently-added.component';
import { APP_CONFIG } from '../../../../config/app-config.interface';
import { environment } from '../../../../environments/environment.test';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { SearchServiceStub } from '../../../shared/testing/search-service.stub';
import { SearchService } from '../../../core/shared/search/search.service';
import { VarDirective } from '../../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('CollectionRecentlyAddedComponent', () => {
let component: CollectionRecentlyAddedComponent;
let fixture: ComponentFixture<CollectionRecentlyAddedComponent>;
let activatedRoute: ActivatedRouteStub;
let paginationService: PaginationServiceStub;
let searchService: SearchServiceStub;
beforeEach(async () => {
activatedRoute = new ActivatedRouteStub();
paginationService = new PaginationServiceStub();
searchService = new SearchServiceStub();
await TestBed.configureTestingModule({
declarations: [
CollectionRecentlyAddedComponent,
VarDirective,
],
imports: [
TranslateModule.forRoot(),
],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: APP_CONFIG, useValue: environment },
{ provide: PaginationService, useValue: paginationService },
{ provide: SearchService, useValue: SearchServiceStub },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(CollectionRecentlyAddedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,82 +0,0 @@
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { Item } from '../../../core/shared/item.model';
import { switchMap, map, startWith, take } from 'rxjs/operators';
import { getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { BROWSE_LINKS_TO_FOLLOW } from '../../../core/browse/browse.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../../core/cache/models/sort-options.model';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
import { SearchService } from '../../../core/shared/search/search.service';
import { Collection } from '../../../core/shared/collection.model';
import { ActivatedRoute, Data } from '@angular/router';
import { fadeIn } from '../../../shared/animations/fade';
@Component({
selector: 'ds-collection-recently-added',
templateUrl: './collection-recently-added.component.html',
styleUrls: ['./collection-recently-added.component.scss'],
animations: [fadeIn],
})
export class CollectionRecentlyAddedComponent implements OnInit, OnDestroy {
paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions;
collectionRD$: Observable<RemoteData<Collection>>;
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
protected paginationService: PaginationService,
protected route: ActivatedRoute,
protected searchService: SearchService,
) {
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: 'cp',
currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize,
});
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
}
ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe(
map((data: Data) => data.dso as RemoteData<Collection>),
take(1),
);
this.itemRD$ = observableCombineLatest([
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig),
this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig),
]).pipe(
switchMap(([currentPagination, currentSort]: [PaginationComponentOptions, SortOptions]) => this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((rd: RemoteData<Collection>) => rd.payload.id),
switchMap((id: string) => this.searchService.search<Item>(
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe(
toDSpaceObjectListRD()
) as Observable<RemoteData<PaginatedList<Item>>>),
startWith(undefined), // Make sure switching pages shows loading component
)),
);
}
ngOnDestroy(): void {
this.paginationService.clearPagination(this.paginationConfig.id);
}
}

View File

@@ -19,6 +19,8 @@ import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-co
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { BrowseByGuard } from '../browse-by/browse-by-guard';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -56,7 +58,16 @@ import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: ComcolSearchSectionComponent,
},
{
path: 'subcoms-cols',
pathMatch: 'full',
component: SubComColSectionComponent, component: SubComColSectionComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { breadcrumbKey: 'community.subcoms-cols' },
}, },
{ {
path: 'browse/:id', path: 'browse/:id',

View File

@@ -11,6 +11,7 @@ describe('SubComColSectionComponent', () => {
beforeEach(async () => { beforeEach(async () => {
activatedRoute = new ActivatedRouteStub(); activatedRoute = new ActivatedRouteStub();
activatedRoute.parent = new ActivatedRouteStub();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ declarations: [

View File

@@ -20,7 +20,7 @@ export class SubComColSectionComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.community$ = this.route.data.pipe( this.community$ = this.route.parent.data.pipe(
map((data: Data) => (data.dso as RemoteData<Community>).payload), map((data: Data) => (data.dso as RemoteData<Community>).payload),
); );
} }

View File

@@ -8,6 +8,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { APP_CONFIG, AppConfig } from '../../config/app-config.interface';
/** /**
* This component renders a search page using a configuration as input. * This component renders a search page using a configuration as input.
@@ -32,7 +33,9 @@ export class ConfigurationSearchPageComponent extends SearchComponent {
protected windowService: HostWindowService, protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService, protected routeService: RouteService,
protected router: Router) { protected router: Router,
super(service, sidebarService, windowService, searchConfigService, routeService, router); @Inject(APP_CONFIG) protected appConfig: AppConfig,
) {
super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig);
} }
} }

View File

@@ -55,16 +55,21 @@ export class ComcolPageBrowseByComponent implements OnDestroy, OnInit {
if (this.contentType === 'collection') { if (this.contentType === 'collection') {
comColRoute = getCollectionPageRoute(this.id); comColRoute = getCollectionPageRoute(this.id);
allOptions.push({ allOptions.push({
id: 'recent_submissions', id: 'search',
label: 'collection.page.browse.recent.head', label: 'collection.page.browse.search.head',
routerLink: comColRoute, routerLink: comColRoute,
}); });
} else if (this.contentType === 'community') { } else if (this.contentType === 'community') {
comColRoute = getCommunityPageRoute(this.id); comColRoute = getCommunityPageRoute(this.id);
allOptions.push({
id: 'search',
label: 'collection.page.browse.search.head',
routerLink: comColRoute,
});
allOptions.push({ allOptions.push({
id: 'comcols', id: 'comcols',
label: 'community.all-lists.head', label: 'community.all-lists.head',
routerLink: comColRoute, routerLink: `${comColRoute}/subcoms-cols`,
}); });
} }

View File

@@ -18,6 +18,8 @@ import { FormModule } from '../form/form.module';
import { UploadModule } from '../upload/upload.module'; import { UploadModule } from '../upload/upload.module';
import { ComcolBrowseByComponent } from './sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolBrowseByComponent } from './sections/comcol-browse-by/comcol-browse-by.component';
import { BrowseByModule } from '../../browse-by/browse-by.module'; import { BrowseByModule } from '../../browse-by/browse-by.module';
import { SearchModule } from '../search/search.module';
import { ComcolSearchSectionComponent } from './sections/comcol-search-section/comcol-search-section.component';
const COMPONENTS = [ const COMPONENTS = [
ComcolPageContentComponent, ComcolPageContentComponent,
@@ -33,6 +35,7 @@ const COMPONENTS = [
ComcolRoleComponent, ComcolRoleComponent,
ThemedComcolPageHandleComponent, ThemedComcolPageHandleComponent,
ComcolBrowseByComponent, ComcolBrowseByComponent,
ComcolSearchSectionComponent,
]; ];
@NgModule({ @NgModule({
@@ -45,6 +48,7 @@ const COMPONENTS = [
SharedModule, SharedModule,
UploadModule, UploadModule,
BrowseByModule, BrowseByModule,
SearchModule,
], ],
exports: [ exports: [
...COMPONENTS, ...COMPONENTS,

View File

@@ -0,0 +1,7 @@
<ds-themed-search
[configuration]="(comcol$ | async)?.type"
[showSidebar]="showSidebar$ | async"
[showScopeSelector]="false"
[hideScopeInUrl]="true"
[scope]="(comcol$ | async)?.id">
</ds-themed-search>

View File

@@ -0,0 +1,35 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComcolSearchSectionComponent } from './comcol-search-section.component';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { environment } from '../../../../../environments/environment.test';
describe('ComcolSearchSectionComponent', () => {
let component: ComcolSearchSectionComponent;
let fixture: ComponentFixture<ComcolSearchSectionComponent>;
let route: ActivatedRouteStub;
beforeEach(async () => {
route = new ActivatedRouteStub();
await TestBed.configureTestingModule({
declarations: [
ComcolSearchSectionComponent,
],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: ActivatedRoute, useValue: route },
],
}).compileComponents();
fixture = TestBed.createComponent(ComcolSearchSectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,48 @@
import { Component, OnInit, Inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ActivatedRoute, Data } from '@angular/router';
import { map } from 'rxjs/operators';
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { Community } from '../../../../core/shared/community.model';
import { Collection } from '../../../../core/shared/collection.model';
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
import { hasValue } from '../../../empty.util';
/**
* The search tab on community & collection pages
*/
@Component({
selector: 'ds-comcol-search-section',
templateUrl: './comcol-search-section.component.html',
styleUrls: ['./comcol-search-section.component.scss'],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService,
},
],
})
export class ComcolSearchSectionComponent implements OnInit {
comcol$: Observable<Community | Collection>;
showSidebar$: Observable<boolean>;
constructor(
@Inject(APP_CONFIG) public appConfig: AppConfig,
protected route: ActivatedRoute,
) {
}
ngOnInit(): void {
this.comcol$ = this.route.data.pipe(
map((data: Data) => (data.dso as RemoteData<Community | Collection>).payload),
);
this.showSidebar$ = this.comcol$.pipe(
map((comcol: Community | Collection) => hasValue(comcol) && this.appConfig[comcol.type as any].searchSection.showSidebar),
);
}
}

View File

@@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core'; import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { isNotEmpty } from '../empty.util'; import { isNotEmpty, hasValue } from '../empty.util';
import { SearchService } from '../../core/shared/search/search.service'; import { SearchService } from '../../core/shared/search/search.service';
import { currentPath } from '../utils/route.utils'; import { currentPath } from '../utils/route.utils';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
@@ -39,6 +39,11 @@ export class SearchFormComponent implements OnChanges {
@Input() @Input()
scope = ''; scope = '';
/**
* Hides the scope in the url, this can be useful when you hardcode the scope in another way
*/
@Input() hideScopeInUrl = false;
selectedScope: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined); selectedScope: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
@Input() currentUrl: string; @Input() currentUrl: string;
@@ -122,6 +127,9 @@ export class SearchFormComponent implements OnChanges {
}, },
data data
); );
if (hasValue(data.scope) && this.hideScopeInUrl) {
delete queryParams.scope;
}
void this.router.navigate(this.getSearchLinkParts(), { void this.router.navigate(this.getSearchLinkParts(), {
queryParams: queryParams, queryParams: queryParams,

View File

@@ -18,6 +18,8 @@ export class ThemedSearchFormComponent extends ThemedComponent<SearchFormCompone
@Input() scope: string; @Input() scope: string;
@Input() hideScopeInUrl: boolean;
@Input() currentUrl: string; @Input() currentUrl: string;
@Input() large: boolean; @Input() large: boolean;
@@ -31,7 +33,15 @@ export class ThemedSearchFormComponent extends ThemedComponent<SearchFormCompone
@Output() submitSearch: EventEmitter<any> = new EventEmitter(); @Output() submitSearch: EventEmitter<any> = new EventEmitter();
protected inAndOutputNames: (keyof SearchFormComponent & keyof this)[] = [ protected inAndOutputNames: (keyof SearchFormComponent & keyof this)[] = [
'query', 'inPlaceSearch', 'scope', 'currentUrl', 'large', 'brandColor', 'searchPlaceholder', 'showScopeSelector', 'query',
'inPlaceSearch',
'scope',
'hideScopeInUrl',
'currentUrl',
'large',
'brandColor',
'searchPlaceholder',
'showScopeSelector',
'submitSearch', 'submitSearch',
]; ];

View File

@@ -27,6 +27,8 @@ import { SearchConfigurationServiceStub } from '../../../../testing/search-confi
import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { FacetValue} from '../../../models/facet-value.model'; import { FacetValue} from '../../../models/facet-value.model';
import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment.test';
describe('SearchHierarchyFilterComponent', () => { describe('SearchHierarchyFilterComponent', () => {
@@ -34,7 +36,7 @@ describe('SearchHierarchyFilterComponent', () => {
let showVocabularyTreeLink: DebugElement; let showVocabularyTreeLink: DebugElement;
const testSearchLink = 'test-search'; const testSearchLink = 'test-search';
const testSearchFilter = 'test-search-filter'; const testSearchFilter = 'subject';
const VocabularyTreeViewComponent = { const VocabularyTreeViewComponent = {
select: new EventEmitter<VocabularyEntryDetail>(), select: new EventEmitter<VocabularyEntryDetail>(),
}; };
@@ -73,6 +75,7 @@ describe('SearchHierarchyFilterComponent', () => {
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: NgbModal, useValue: ngbModal }, { provide: NgbModal, useValue: ngbModal },
{ provide: VocabularyService, useValue: vocabularyService }, { provide: VocabularyService, useValue: vocabularyService },
{ provide: APP_CONFIG, useValue: environment },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false }, { provide: IN_PLACE_SEARCH, useValue: false },
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
@@ -86,7 +89,7 @@ describe('SearchHierarchyFilterComponent', () => {
function init() { function init() {
fixture = TestBed.createComponent(SearchHierarchyFilterComponent); fixture = TestBed.createComponent(SearchHierarchyFilterComponent);
fixture.detectChanges(); fixture.detectChanges();
showVocabularyTreeLink = fixture.debugElement.query(By.css('a#show-test-search-filter-tree')); showVocabularyTreeLink = fixture.debugElement.query(By.css(`a#show-${testSearchFilter}-tree`));
} }
describe('if the vocabulary doesn\'t exist', () => { describe('if the vocabulary doesn\'t exist', () => {

View File

@@ -24,9 +24,11 @@ import { filter, map, take } from 'rxjs/operators';
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { PageInfo } from '../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../../core/shared/page-info.model';
import { environment } from '../../../../../../environments/environment';
import { addOperatorToFilterValue } from '../../../search.utils'; import { addOperatorToFilterValue } from '../../../search.utils';
import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component'; import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component';
import { hasValue } from '../../../../empty.util';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
import { FilterVocabularyConfig } from '../../../../../../config/filter-vocabulary-config';
@Component({ @Component({
selector: 'ds-search-hierarchy-filter', selector: 'ds-search-hierarchy-filter',
@@ -47,6 +49,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
protected router: Router, protected router: Router,
protected modalService: NgbModal, protected modalService: NgbModal,
protected vocabularyService: VocabularyService, protected vocabularyService: VocabularyService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
@@ -67,10 +70,12 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
super.onSubmit(addOperatorToFilterValue(data, 'query')); super.onSubmit(addOperatorToFilterValue(data, 'query'));
} }
ngOnInit() { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
const vocabularyName: string = this.getVocabularyEntry();
if (hasValue(vocabularyName)) {
this.vocabularyExists$ = this.vocabularyService.searchTopEntries( this.vocabularyExists$ = this.vocabularyService.searchTopEntries(
this.getVocabularyEntry(), new PageInfo(), true, false, vocabularyName, new PageInfo(), true, false,
).pipe( ).pipe(
filter(rd => rd.hasCompleted), filter(rd => rd.hasCompleted),
take(1), take(1),
@@ -79,6 +84,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
}), }),
); );
} }
}
/** /**
* Open the vocabulary tree modal popup. * Open the vocabulary tree modal popup.
@@ -93,11 +99,11 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
name: this.getVocabularyEntry(), name: this.getVocabularyEntry(),
closed: true closed: true
}; };
modalRef.result.then((detail: VocabularyEntryDetail) => { void modalRef.result.then((detail: VocabularyEntryDetail) => {
this.selectedValues$ this.subs.push(this.selectedValues$
.pipe(take(1)) .pipe(take(1))
.subscribe((selectedValues) => { .subscribe((selectedValues) => {
this.router.navigate( void this.router.navigate(
[this.searchService.getSearchLink()], [this.searchService.getSearchLink()],
{ {
queryParams: { queryParams: {
@@ -107,16 +113,16 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
}, },
); );
}));
}); });
}).catch();
} }
/** /**
* Returns the matching vocabulary entry for the given search filter. * Returns the matching vocabulary entry for the given search filter.
* These are configurable in the config file. * These are configurable in the config file.
*/ */
getVocabularyEntry() { getVocabularyEntry(): string {
const foundVocabularyConfig = environment.vocabularies.filter((v) => v.filter === this.filterConfig.name); const foundVocabularyConfig: FilterVocabularyConfig[] = this.appConfig.vocabularies.filter((v: FilterVocabularyConfig) => v.filter === this.filterConfig.name);
if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) { if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) {
return foundVocabularyConfig[0].vocabulary; return foundVocabularyConfig[0].vocabulary;
} }

View File

@@ -85,6 +85,7 @@
<ds-themed-search-form *ngIf="searchEnabled" id="search-form" <ds-themed-search-form *ngIf="searchEnabled" id="search-form"
[query]="(searchOptions$ | async)?.query" [query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope" [scope]="(searchOptions$ | async)?.scope"
[hideScopeInUrl]="hideScopeInUrl"
[currentUrl]="searchLink" [currentUrl]="searchLink"
[showScopeSelector]="showScopeSelector" [showScopeSelector]="showScopeSelector"
[inPlaceSearch]="inPlaceSearch" [inPlaceSearch]="inPlaceSearch"

View File

@@ -33,6 +33,8 @@ import { SearchFilterConfig } from './models/search-filter-config.model';
import { FilterType } from './models/filter-type.model'; import { FilterType } from './models/filter-type.model';
import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths';
import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths';
import { environment } from '../../../environments/environment.test';
import { APP_CONFIG } from '../../../config/app-config.interface';
let comp: SearchComponent; let comp: SearchComponent;
let fixture: ComponentFixture<SearchComponent>; let fixture: ComponentFixture<SearchComponent>;
@@ -209,7 +211,8 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
{ {
provide: SEARCH_CONFIG_SERVICE, provide: SEARCH_CONFIG_SERVICE,
useValue: searchConfigurationServiceStub useValue: searchConfigurationServiceStub
} },
{ provide: APP_CONFIG, useValue: environment },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(compType, { }).overrideComponent(compType, {

View File

@@ -31,13 +31,13 @@ import { ViewMode } from '../../core/shared/view-mode.model';
import { SelectionConfig } from './search-results/search-results.component'; import { SelectionConfig } from './search-results/search-results.component';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type'; import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
import { environment } from 'src/environments/environment';
import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../core/submission/models/submission-object.model';
import { SearchFilterConfig } from './models/search-filter-config.model'; import { SearchFilterConfig } from './models/search-filter-config.model';
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths';
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
@Component({ @Component({
selector: 'ds-search', selector: 'ds-search',
@@ -171,6 +171,11 @@ export class SearchComponent implements OnDestroy, OnInit {
*/ */
@Input() scope: string; @Input() scope: string;
/**
* Hides the scope in the url, this can be useful when you hardcode the scope in another way
*/
@Input() hideScopeInUrl: boolean;
/** /**
* The current configuration used during the search * The current configuration used during the search
*/ */
@@ -278,7 +283,9 @@ export class SearchComponent implements OnDestroy, OnInit {
protected windowService: HostWindowService, protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService, protected routeService: RouteService,
protected router: Router) { protected router: Router,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();
} }
@@ -445,8 +452,10 @@ export class SearchComponent implements OnDestroy, OnInit {
let followLinks = [ let followLinks = [
followLink<Item>('thumbnail', { isOptional: true }), followLink<Item>('thumbnail', { isOptional: true }),
followLink<SubmissionObject>('item', { isOptional: true }, followLink<Item>('thumbnail', { isOptional: true })) as any, followLink<SubmissionObject>('item', { isOptional: true }, followLink<Item>('thumbnail', { isOptional: true })) as any,
followLink<Item>('accessStatus', { isOptional: true, shouldEmbed: environment.item.showAccessStatuses }),
]; ];
if (this.appConfig.item.showAccessStatuses) {
followLinks.push(followLink<Item>('accessStatus', { isOptional: true }));
}
if (this.configuration === 'supervision') { if (this.configuration === 'supervision') {
followLinks.push(followLink<WorkspaceItem>('supervisionOrders', { isOptional: true }) as any); followLinks.push(followLink<WorkspaceItem>('supervisionOrders', { isOptional: true }) as any);
} }
@@ -476,7 +485,8 @@ export class SearchComponent implements OnDestroy, OnInit {
* This method should only be called once and is essentially what SearchTrackingComponent used to do (now removed) * This method should only be called once and is essentially what SearchTrackingComponent used to do (now removed)
* @private * @private
*/ */
private subscribeToRoutingEvents() { private subscribeToRoutingEvents(): void {
if (this.trackStatistics) {
this.subs.push( this.subs.push(
this.router.events.pipe( this.router.events.pipe(
filter((event) => event instanceof NavigationStart), filter((event) => event instanceof NavigationStart),
@@ -489,6 +499,7 @@ export class SearchComponent implements OnDestroy, OnInit {
}), }),
); );
} }
}
/** /**
* Get the UUID from a DSO url * Get the UUID from a DSO url

View File

@@ -43,6 +43,7 @@ export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
'trackStatistics', 'trackStatistics',
'query', 'query',
'scope', 'scope',
'hideScopeInUrl',
'resultFound', 'resultFound',
'deselectObject', 'deselectObject',
'selectObject', 'selectObject',
@@ -94,6 +95,8 @@ export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
@Input() scope: string; @Input() scope: string;
@Input() hideScopeInUrl: boolean;
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter(); @Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();

View File

@@ -3,10 +3,12 @@
<div class="row-with-sidebar row-offcanvas row-offcanvas-left" <div class="row-with-sidebar row-offcanvas row-offcanvas-left"
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'"> [@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
<div id="{{id}}-sidebar-content" <div id="{{id}}-sidebar-content"
[class.invisible]="(isSidebarCollapsed$ | async) === true && (isXsOrSm$ | async) === true"
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}"> class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container> <ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
</div> </div>
<div class="col-12 col-md-{{12 - sideBarWidth}}"> <div class="col-12 col-md-{{12 - sideBarWidth}}"
[class.invisible]="(isSidebarCollapsed$ | async) === false && (isXsOrSm$ | async) === true">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div> </div>

View File

@@ -1,19 +1,19 @@
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { convertToParamMap, Params } from '@angular/router'; import { ActivatedRoute, convertToParamMap, Data, Params } from '@angular/router';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
export class ActivatedRouteStub { export class ActivatedRouteStub {
private _testParams?: any; private _testParams?: Params;
private _testData?: any; private _testData?: Data;
// ActivatedRoute.params is Observable // ActivatedRoute.params is Observable
private subject?: BehaviorSubject<any> = new BehaviorSubject(this.testParams); private subject?: BehaviorSubject<Params> = new BehaviorSubject(this.testParams);
private dataSubject?: BehaviorSubject<any> = new BehaviorSubject(this.testData); private dataSubject?: BehaviorSubject<Data> = new BehaviorSubject(this.testData);
params = this.subject.asObservable(); params = this.subject.asObservable();
queryParams = this.subject.asObservable(); queryParams = this.subject.asObservable();
paramMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params))); paramMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));
parent: ActivatedRoute | ActivatedRouteStub;
queryParamMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params))); queryParamMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));
data = this.dataSubject.asObservable(); data = this.dataSubject.asObservable();
@@ -35,17 +35,17 @@ export class ActivatedRouteStub {
return this._testParams; return this._testParams;
} }
set testParams(params: {}) { set testParams(params: Params) {
this._testParams = params; this._testParams = params;
this.subject.next(params); this.subject.next(params);
} }
// Test data // Test data
get testData() { get testData() {
return this._testParams; return this._testData;
} }
set testData(data: {}) { set testData(data: Data) {
this._testData = data; this._testData = data;
this.dataSubject.next(data); this.dataSubject.next(data);
} }

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Angulartics2 } from 'angulartics2'; import { Angulartics2, EventTrack } from 'angulartics2';
import { StatisticsService } from '../statistics.service'; import { StatisticsService } from '../statistics.service';
/** /**
@@ -23,7 +23,7 @@ export class Angulartics2DSpace {
.subscribe((event) => this.eventTrack(event)); .subscribe((event) => this.eventTrack(event));
} }
private eventTrack(event) { private eventTrack(event: Partial<EventTrack>): void {
if (event.action === 'page_view') { if (event.action === 'page_view') {
this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer); this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer);
} else if (event.action === 'search') { } else if (event.action === 'search') {
@@ -32,7 +32,7 @@ export class Angulartics2DSpace {
event.properties.page, event.properties.page,
event.properties.sort, event.properties.sort,
event.properties.filters, event.properties.filters,
event.properties.clickedObject, event.properties.clickedObject?.split('?')[0],
); );
} }
} }

View File

@@ -1108,9 +1108,7 @@
"collection.logo": "Collection logo", "collection.logo": "Collection logo",
"collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.search.head": "Search",
"collection.page.browse.recent.empty": "No items to show",
"collection.page.edit": "Edit this collection", "collection.page.edit": "Edit this collection",
@@ -1120,6 +1118,8 @@
"collection.page.news": "News", "collection.page.news": "News",
"collection.search.results.head": "Search Results",
"collection.select.confirm": "Confirm selected", "collection.select.confirm": "Confirm selected",
"collection.select.empty": "No collections to show", "collection.select.empty": "No collections to show",
@@ -1196,6 +1196,8 @@
"community.browse.logo": "Browse for a community logo", "community.browse.logo": "Browse for a community logo",
"community.subcoms-cols.breadcrumbs": "Subcommunities and Collections",
"community.create.head": "Create a Community", "community.create.head": "Create a Community",
"community.create.notifications.success": "Successfully created the Community", "community.create.notifications.success": "Successfully created the Community",
@@ -1348,6 +1350,8 @@
"community.all-lists.head": "Subcommunities and Collections", "community.all-lists.head": "Subcommunities and Collections",
"community.search.results.head": "Search Results",
"community.sub-collection-list.head": "Collections in this Community", "community.sub-collection-list.head": "Collections in this Community",
"community.sub-community-list.head": "Communities in this Community", "community.sub-community-list.head": "Communities in this Community",

View File

@@ -8,6 +8,7 @@ import { SubmissionConfig } from './submission-config.interface';
import { FormConfig } from './form-config.interfaces'; import { FormConfig } from './form-config.interfaces';
import { LangConfig } from './lang-config.interface'; import { LangConfig } from './lang-config.interface';
import { ItemConfig } from './item-config.interface'; import { ItemConfig } from './item-config.interface';
import { CommunityPageConfig } from './community-page-config.interface';
import { CollectionPageConfig } from './collection-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface';
import { ThemeConfig } from './theme.config'; import { ThemeConfig } from './theme.config';
import { AuthConfig } from './auth-config.interfaces'; import { AuthConfig } from './auth-config.interfaces';
@@ -41,6 +42,7 @@ interface AppConfig extends Config {
communityList: CommunityListConfig; communityList: CommunityListConfig;
homePage: HomeConfig; homePage: HomeConfig;
item: ItemConfig; item: ItemConfig;
community: CommunityPageConfig;
collection: CollectionPageConfig; collection: CollectionPageConfig;
themes: ThemeConfig[]; themes: ThemeConfig[];
mediaViewer: MediaViewerConfig; mediaViewer: MediaViewerConfig;

View File

@@ -1,7 +1,18 @@
import { Config } from './config.interface'; import { Config } from './config.interface';
/**
* Collection Page Config
*/
export interface CollectionPageConfig extends Config { export interface CollectionPageConfig extends Config {
searchSection: CollectionSearchSectionConfig;
edit: { edit: {
undoTimeout: number; undoTimeout: number;
}; };
} }
/**
* Config related to the collection's search tab
*/
export interface CollectionSearchSectionConfig {
showSidebar: boolean;
}

View File

@@ -0,0 +1,15 @@
import { Config } from './config.interface';
/**
* Community Page Config
*/
export interface CommunityPageConfig extends Config {
searchSection: CommunitySearchSectionConfig;
}
/**
* Config related to the community's search tab
*/
export interface CommunitySearchSectionConfig {
showSidebar: boolean;
}

View File

@@ -23,6 +23,7 @@ import { HomeConfig } from './homepage-config.interface';
import { MarkdownConfig } from './markdown-config.interface'; 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 {QualityAssuranceConfig} from './quality-assurance.config'; import {QualityAssuranceConfig} from './quality-assurance.config';
export class DefaultAppConfig implements AppConfig { export class DefaultAppConfig implements AppConfig {
@@ -291,8 +292,18 @@ export class DefaultAppConfig implements AppConfig {
} }
}; };
// Community Page Config
community: CommunityPageConfig = {
searchSection: {
showSidebar: true,
},
};
// Collection Page Config // Collection Page Config
collection: CollectionPageConfig = { collection: CollectionPageConfig = {
searchSection: {
showSidebar: true,
},
edit: { edit: {
undoTimeout: 10000 // 10 seconds undoTimeout: 10000 // 10 seconds
} }

View File

@@ -257,7 +257,15 @@ export const environment: BuildConfig = {
pageSize: 5 pageSize: 5
} }
}, },
community: {
searchSection: {
showSidebar: true,
},
},
collection: { collection: {
searchSection: {
showSidebar: true,
},
edit: { edit: {
undoTimeout: 10000 // 10 seconds undoTimeout: 10000 // 10 seconds
} }