mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
Merge remote-tracking branch 'origin/main' into #1110
This commit is contained in:
@@ -179,17 +179,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/processes/new'
|
link: '/processes/new'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'new_item_version',
|
// {
|
||||||
parentID: 'new',
|
// id: 'new_item_version',
|
||||||
active: false,
|
// parentID: 'new',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.new_item_version',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.new_item_version',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
|
|
||||||
/* Edit */
|
/* Edit */
|
||||||
{
|
{
|
||||||
@@ -243,47 +244,35 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Curation tasks */
|
|
||||||
{
|
|
||||||
id: 'curation_tasks',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.curation_task',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'filter',
|
|
||||||
index: 7
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Statistics */
|
/* Statistics */
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'statistics_task',
|
// {
|
||||||
active: false,
|
// id: 'statistics_task',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.statistics_task',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.statistics_task',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
icon: 'chart-bar',
|
// } as LinkMenuItemModel,
|
||||||
index: 8
|
// icon: 'chart-bar',
|
||||||
},
|
// index: 8
|
||||||
|
// },
|
||||||
|
|
||||||
/* Control Panel */
|
/* Control Panel */
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'control_panel',
|
// {
|
||||||
active: false,
|
// id: 'control_panel',
|
||||||
visible: isSiteAdmin,
|
// active: false,
|
||||||
model: {
|
// visible: isSiteAdmin,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.control_panel',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.control_panel',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
icon: 'cogs',
|
// } as LinkMenuItemModel,
|
||||||
index: 9
|
// icon: 'cogs',
|
||||||
},
|
// index: 9
|
||||||
|
// },
|
||||||
|
|
||||||
/* Processes */
|
/* Processes */
|
||||||
{
|
{
|
||||||
@@ -324,42 +313,45 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
index: 3,
|
index: 3,
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'export_community',
|
// {
|
||||||
parentID: 'export',
|
// id: 'export_community',
|
||||||
active: false,
|
// parentID: 'export',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.export_community',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.export_community',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
shouldPersistOnRouteChange: true
|
// } as LinkMenuItemModel,
|
||||||
},
|
// shouldPersistOnRouteChange: true
|
||||||
{
|
// },
|
||||||
id: 'export_collection',
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
parentID: 'export',
|
// {
|
||||||
active: false,
|
// id: 'export_collection',
|
||||||
visible: true,
|
// parentID: 'export',
|
||||||
model: {
|
// active: false,
|
||||||
type: MenuItemType.LINK,
|
// visible: true,
|
||||||
text: 'menu.section.export_collection',
|
// model: {
|
||||||
link: ''
|
// type: MenuItemType.LINK,
|
||||||
} as LinkMenuItemModel,
|
// text: 'menu.section.export_collection',
|
||||||
shouldPersistOnRouteChange: true
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
{
|
// shouldPersistOnRouteChange: true
|
||||||
id: 'export_item',
|
// },
|
||||||
parentID: 'export',
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
active: false,
|
// {
|
||||||
visible: true,
|
// id: 'export_item',
|
||||||
model: {
|
// parentID: 'export',
|
||||||
type: MenuItemType.LINK,
|
// active: false,
|
||||||
text: 'menu.section.export_item',
|
// visible: true,
|
||||||
link: ''
|
// model: {
|
||||||
} as LinkMenuItemModel,
|
// type: MenuItemType.LINK,
|
||||||
shouldPersistOnRouteChange: true
|
// text: 'menu.section.export_item',
|
||||||
},
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// shouldPersistOnRouteChange: true
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||||
|
|
||||||
@@ -406,17 +398,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'file-import',
|
icon: 'file-import',
|
||||||
index: 2
|
index: 2
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'import_batch',
|
// {
|
||||||
parentID: 'import',
|
// id: 'import_batch',
|
||||||
active: false,
|
// parentID: 'import',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.import_batch',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.import_batch',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
}
|
// } as LinkMenuItemModel,
|
||||||
|
// }
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
@@ -563,17 +556,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/access-control/groups'
|
link: '/access-control/groups'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'access_control_authorizations',
|
// {
|
||||||
parentID: 'access_control',
|
// id: 'access_control_authorizations',
|
||||||
active: false,
|
// parentID: 'access_control',
|
||||||
visible: authorized,
|
// active: false,
|
||||||
model: {
|
// visible: authorized,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.access_control_authorizations',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.access_control_authorizations',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: 'access_control',
|
id: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
|
@@ -18,7 +18,7 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
|
|||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
getAllSucceededRemoteData,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload,
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
@@ -29,6 +29,7 @@ import { SearchResult } from '../../../../shared/search/search-result.model';
|
|||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -146,6 +147,11 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
modalComp.repeatable = true;
|
modalComp.repeatable = true;
|
||||||
modalComp.listId = this.listId;
|
modalComp.listId = this.listId;
|
||||||
modalComp.item = this.item;
|
modalComp.item = this.item;
|
||||||
|
this.item.owningCollection.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
).subscribe((collection: Collection) => {
|
||||||
|
modalComp.collection = collection;
|
||||||
|
});
|
||||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
||||||
selectableObjects.forEach((searchResult) => {
|
selectableObjects.forEach((searchResult) => {
|
||||||
const relatedItem: Item = searchResult.indexableObject;
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
@@ -27,7 +27,10 @@ describe('MyDSpaceConfigurationService', () => {
|
|||||||
scope: ''
|
scope: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const backendFilters = [new SearchFilter('f.namedresourcetype', ['another value']), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'])];
|
const backendFilters = [
|
||||||
|
new SearchFilter('f.namedresourcetype', ['another value']),
|
||||||
|
new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'], 'equals')
|
||||||
|
];
|
||||||
|
|
||||||
const spy = jasmine.createSpyObj('RouteService', {
|
const spy = jasmine.createSpyObj('RouteService', {
|
||||||
getQueryParameterValue: observableOf(value1),
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
[configurationList]="(configurationList$ | async)"
|
[configurationList]="(configurationList$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
[viewModeList]="viewModeList"
|
[viewModeList]="viewModeList"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[refreshFilters]="refreshFilters.asObservable()"
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
@@ -28,6 +30,8 @@
|
|||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[refreshFilters]="refreshFilters.asObservable()"
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
|
@@ -45,6 +45,7 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
pagination.id = 'mydspace-results-pagination';
|
pagination.id = 'mydspace-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
|
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
@@ -52,7 +53,8 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
getEndpoint: observableOf('discover/search/objects'),
|
getEndpoint: observableOf('discover/search/objects'),
|
||||||
getSearchLink: '/mydspace',
|
getSearchLink: '/mydspace',
|
||||||
getScopes: observableOf(['test-scope']),
|
getScopes: observableOf(['test-scope']),
|
||||||
setServiceOptions: {}
|
setServiceOptions: {},
|
||||||
|
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||||
});
|
});
|
||||||
const configurationParam = 'default';
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
@@ -188,4 +190,24 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when stable', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have initialized the sortOptions$ observable', (done) => {
|
||||||
|
|
||||||
|
comp.sortOptions$.subscribe((sortOptions) => {
|
||||||
|
|
||||||
|
expect(sortOptions.length).toEqual(2);
|
||||||
|
expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC));
|
||||||
|
expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { map, switchMap, tap, } from 'rxjs/operators';
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
@@ -29,6 +29,8 @@ import { ViewMode } from '../core/shared/view-mode.model';
|
|||||||
import { MyDSpaceRequest } from '../core/data/request.models';
|
import { MyDSpaceRequest } from '../core/data/request.models';
|
||||||
import { SearchResult } from '../shared/search/search-result.model';
|
import { SearchResult } from '../shared/search/search-result.model';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { RouteService } from '../core/services/route.service';
|
||||||
|
|
||||||
export const MYDSPACE_ROUTE = '/mydspace';
|
export const MYDSPACE_ROUTE = '/mydspace';
|
||||||
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||||
@@ -71,6 +73,11 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current available sort options
|
||||||
|
*/
|
||||||
|
sortOptions$: Observable<SortOptions[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current relevant scopes
|
* The current relevant scopes
|
||||||
*/
|
*/
|
||||||
@@ -109,7 +116,8 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private sidebarService: SidebarService,
|
private sidebarService: SidebarService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService,
|
||||||
|
private routeService: RouteService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,12 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const configuration$ = this.searchConfigService.getCurrentConfiguration('workspace');
|
||||||
|
const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(configuration$, this.service);
|
||||||
|
|
||||||
|
this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$);
|
||||||
|
this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,11 +31,14 @@
|
|||||||
<ng-template #sidebarContent>
|
<ng-template #sidebarContent>
|
||||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
>
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
|
(toggleSidebar)="closeSidebar()">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -40,12 +40,14 @@ const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
|||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
|
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
search: mockResults,
|
search: mockResults,
|
||||||
getSearchLink: '/search',
|
getSearchLink: '/search',
|
||||||
getScopes: observableOf(['test-scope'])
|
getScopes: observableOf(['test-scope']),
|
||||||
|
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||||
});
|
});
|
||||||
const configurationParam = 'default';
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
@@ -181,4 +183,24 @@ describe('SearchComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when stable', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have initialized the sortOptions$ observable', (done) => {
|
||||||
|
|
||||||
|
comp.sortOptions$.subscribe((sortOptions) => {
|
||||||
|
|
||||||
|
expect(sortOptions.length).toEqual(2);
|
||||||
|
expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC));
|
||||||
|
expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
import { startWith, switchMap, } from 'rxjs/operators';
|
import { startWith, switchMap } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isEmpty } from '../shared/empty.util';
|
||||||
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.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';
|
||||||
@@ -16,8 +16,9 @@ import { SearchResult } from '../shared/search/search-result.model';
|
|||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { currentPath } from '../shared/utils/route.utils';
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
import { Router } from '@angular/router';
|
import { Router} from '@angular/router';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -47,6 +48,11 @@ export class SearchComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current available sort options
|
||||||
|
*/
|
||||||
|
sortOptions$: Observable<SortOptions[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current relevant scopes
|
* The current relevant scopes
|
||||||
*/
|
*/
|
||||||
@@ -129,9 +135,15 @@ export class SearchComponent implements OnInit {
|
|||||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||||
);
|
);
|
||||||
if (!isNotEmpty(this.configuration$)) {
|
if (isEmpty(this.configuration$)) {
|
||||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
this.configuration$ = this.searchConfigService.getCurrentConfiguration('default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(this.configuration$, this.service);
|
||||||
|
|
||||||
|
this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$);
|
||||||
|
this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -210,4 +210,11 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ngOnDestroy', () => {
|
||||||
|
it('does NOT call router.navigate', () => {
|
||||||
|
component.ngOnDestroy();
|
||||||
|
expect(router.navigate).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -405,7 +405,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@HostListener('window:beforeunload')
|
@HostListener('window:beforeunload')
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -161,6 +161,7 @@ import { ShortLivedToken } from './auth/models/short-lived-token.model';
|
|||||||
import { UsageReport } from './statistics/models/usage-report.model';
|
import { UsageReport } from './statistics/models/usage-report.model';
|
||||||
import { RootDataService } from './data/root-data.service';
|
import { RootDataService } from './data/root-data.service';
|
||||||
import { Root } from './data/root.model';
|
import { Root } from './data/root.model';
|
||||||
|
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -340,6 +341,7 @@ export const models =
|
|||||||
Registration,
|
Registration,
|
||||||
UsageReport,
|
UsageReport,
|
||||||
Root,
|
Root,
|
||||||
|
SearchConfig
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -23,7 +23,10 @@ describe('SearchConfigurationService', () => {
|
|||||||
scope: ''
|
scope: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
|
const backendFilters = [
|
||||||
|
new SearchFilter('f.author', ['another value']),
|
||||||
|
new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals')
|
||||||
|
];
|
||||||
|
|
||||||
const routeService = jasmine.createSpyObj('RouteService', {
|
const routeService = jasmine.createSpyObj('RouteService', {
|
||||||
getQueryParameterValue: observableOf(value1),
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
import {
|
||||||
import { filter, map, startWith } from 'rxjs/operators';
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
merge as observableMerge,
|
||||||
|
Observable,
|
||||||
|
Subscription
|
||||||
|
} from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchOptions } from '../../../shared/search/search-options.model';
|
import { SearchOptions } from '../../../shared/search/search-options.model';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||||
@@ -11,9 +18,15 @@ import { RemoteData } from '../../data/remote-data';
|
|||||||
import { DSpaceObjectType } from '../dspace-object-type.model';
|
import { DSpaceObjectType } from '../dspace-object-type.model';
|
||||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||||
import { RouteService } from '../../services/route.service';
|
import { RouteService } from '../../services/route.service';
|
||||||
import { getFirstSucceededRemoteData } from '../operators';
|
import {
|
||||||
|
getAllSucceededRemoteDataPayload,
|
||||||
|
getFirstSucceededRemoteData
|
||||||
|
} from '../operators';
|
||||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { SearchConfig } from './search-filters/search-config.model';
|
||||||
|
import { SearchService } from './search.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
import { PaginationService } from '../../pagination/pagination.service';
|
import { PaginationService } from '../../pagination/pagination.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +181,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
if (hasNoValue(filters.find((f) => f.key === realKey))) {
|
if (hasNoValue(filters.find((f) => f.key === realKey))) {
|
||||||
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
|
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
|
||||||
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
|
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
|
||||||
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']']));
|
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filters.push(new SearchFilter(key, filterParams[key]));
|
filters.push(new SearchFilter(key, filterParams[key]));
|
||||||
@@ -194,6 +207,60 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
return this.routeService.getQueryParamsWithPrefix('f.');
|
return this.routeService.getQueryParamsWithPrefix('f.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an observable of SearchConfig every time the configuration$ stream emits.
|
||||||
|
* @param configuration$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
getConfigurationSearchConfigObservable(configuration$: Observable<string>, service: SearchService): Observable<SearchConfig> {
|
||||||
|
return configuration$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)),
|
||||||
|
getAllSucceededRemoteDataPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every time searchConfig change (after a configuration change) it update the navigation with the default sort option
|
||||||
|
* and emit the new paginateSearchOptions value.
|
||||||
|
* @param configuration$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
initializeSortOptionsFromConfiguration(searchConfig$: Observable<SearchConfig>) {
|
||||||
|
const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([
|
||||||
|
of(searchConfig),
|
||||||
|
this.paginatedSearchOptions.pipe(take(1))
|
||||||
|
]))).subscribe(([searchConfig, searchOptions]) => {
|
||||||
|
const field = searchConfig.sortOptions[0].name;
|
||||||
|
const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC;
|
||||||
|
const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||||
|
sort: new SortOptions(field, direction)
|
||||||
|
});
|
||||||
|
this.paginationService.updateRoute(this.paginationID,
|
||||||
|
{
|
||||||
|
sortDirection: updateValue.sort.direction,
|
||||||
|
sortField: updateValue.sort.field,
|
||||||
|
});
|
||||||
|
this.paginatedSearchOptions.next(updateValue);
|
||||||
|
});
|
||||||
|
this.subs.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an observable of available SortOptions[] every time the searchConfig$ stream emits.
|
||||||
|
* @param searchConfig$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
getConfigurationSortOptionsObservable(searchConfig$: Observable<SearchConfig>): Observable<SortOptions[]> {
|
||||||
|
return searchConfig$.pipe(map((searchConfig) => {
|
||||||
|
const sortOptions = [];
|
||||||
|
searchConfig.sortOptions.forEach(sortOption => {
|
||||||
|
sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC));
|
||||||
|
sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC));
|
||||||
|
});
|
||||||
|
return sortOptions;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update
|
* Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update
|
||||||
* @param {SearchOptions} defaults Default values for when no parameters are available
|
* @param {SearchOptions} defaults Default values for when no parameters are available
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { SEARCH_CONFIG } from './search-config.resource-type';
|
||||||
|
import { typedObject } from '../../../cache/builders/build-decorators';
|
||||||
|
import { CacheableObject } from '../../../cache/object-cache.reducer';
|
||||||
|
import { HALLink } from '../../hal-link.model';
|
||||||
|
import { ResourceType } from '../../resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for a search
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class SearchConfig implements CacheableObject {
|
||||||
|
static type = SEARCH_CONFIG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of this search configuration.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured filters.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
filters: FilterConfig[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured sort options.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
sortOptions: SortOption[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Item
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
facets: HALLink;
|
||||||
|
objects: HALLink;
|
||||||
|
self: HALLink;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model filter's configuration.
|
||||||
|
*/
|
||||||
|
export interface FilterConfig {
|
||||||
|
filter: string;
|
||||||
|
hasFacets: boolean;
|
||||||
|
operators: OperatorConfig[];
|
||||||
|
openByDefault: boolean;
|
||||||
|
pageSize: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model sort option's configuration.
|
||||||
|
*/
|
||||||
|
export interface SortOption {
|
||||||
|
name: string;
|
||||||
|
sortOrder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model operator's configuration.
|
||||||
|
*/
|
||||||
|
export interface OperatorConfig {
|
||||||
|
operator: string;
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import {ResourceType} from '../../resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for SearchConfig
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SEARCH_CONFIG = new ResourceType('discover');
|
@@ -240,5 +240,55 @@ describe('SearchService', () => {
|
|||||||
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when getSearchConfigurationFor is called without a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
|
spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough();
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getSearchConfigurationFor(null).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call send containing a request with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: endPoint }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getSearchConfigurationFor is called with a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
const scope = 'test';
|
||||||
|
const requestUrl = endPoint + '?scope=' + scope;
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getSearchConfigurationFor(scope).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call send containing a request with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -37,6 +37,7 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl
|
|||||||
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
|
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
|
||||||
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
||||||
import { FacetValues } from '../../../shared/search/facet-values.model';
|
import { FacetValues } from '../../../shared/search/facet-values.model';
|
||||||
|
import { SearchConfig } from './search-filters/search-config.model';
|
||||||
import { PaginationService } from '../../pagination/pagination.service';
|
import { PaginationService } from '../../pagination/pagination.service';
|
||||||
import { SearchConfigurationService } from './search-configuration.service';
|
import { SearchConfigurationService } from './search-configuration.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -46,6 +47,12 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService implements OnDestroy {
|
export class SearchService implements OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint link path for retrieving search configurations
|
||||||
|
*/
|
||||||
|
private configurationLinkPath = 'discover/search';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint link path for retrieving general search results
|
* Endpoint link path for retrieving general search results
|
||||||
*/
|
*/
|
||||||
@@ -229,15 +236,7 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getConfigUrl(url: string, scope?: string, configurationName?: string) {
|
||||||
* Request the filter configuration for a given scope or the whole repository
|
|
||||||
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
|
||||||
* @param {string} configurationName the name of the configuration
|
|
||||||
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
|
|
||||||
*/
|
|
||||||
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
|
||||||
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
|
||||||
map((url: string) => {
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (isNotEmpty(scope)) {
|
if (isNotEmpty(scope)) {
|
||||||
@@ -253,7 +252,17 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the filter configuration for a given scope or the whole repository
|
||||||
|
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
||||||
|
* @param {string} configurationName the name of the configuration
|
||||||
|
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
|
||||||
|
*/
|
||||||
|
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||||
|
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
||||||
|
map((url: string) => this.getConfigUrl(url, scope, configurationName)),
|
||||||
);
|
);
|
||||||
|
|
||||||
href$.pipe(take(1)).subscribe((url: string) => {
|
href$.pipe(take(1)).subscribe((url: string) => {
|
||||||
@@ -398,6 +407,25 @@ export class SearchService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the search configuration for a given scope or the whole repository
|
||||||
|
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
||||||
|
* @param {string} configurationName the name of the configuration
|
||||||
|
* @returns {Observable<RemoteData<SearchConfig[]>>} The found configuration
|
||||||
|
*/
|
||||||
|
getSearchConfigurationFor(scope?: string, configurationName?: string ): Observable<RemoteData<SearchConfig>> {
|
||||||
|
const href$ = this.halService.getEndpoint(this.configurationLinkPath).pipe(
|
||||||
|
map((url: string) => this.getConfigUrl(url, scope, configurationName)),
|
||||||
|
);
|
||||||
|
|
||||||
|
href$.pipe(take(1)).subscribe((url: string) => {
|
||||||
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
this.requestService.send(request, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdb.buildFromHref(href$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page
|
||||||
*/
|
*/
|
||||||
|
@@ -10,6 +10,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
|
|||||||
import { createPaginatedList } from '../../../testing/utils.test';
|
import { createPaginatedList } from '../../../testing/utils.test';
|
||||||
import { Collection } from '../../../../core/shared/collection.model';
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||||
|
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||||
|
|
||||||
describe('AuthorizedCollectionSelectorComponent', () => {
|
describe('AuthorizedCollectionSelectorComponent', () => {
|
||||||
let component: AuthorizedCollectionSelectorComponent;
|
let component: AuthorizedCollectionSelectorComponent;
|
||||||
@@ -18,6 +19,8 @@ describe('AuthorizedCollectionSelectorComponent', () => {
|
|||||||
let collectionService;
|
let collectionService;
|
||||||
let collection;
|
let collection;
|
||||||
|
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
collection = Object.assign(new Collection(), {
|
collection = Object.assign(new Collection(), {
|
||||||
id: 'authorized-collection'
|
id: 'authorized-collection'
|
||||||
@@ -25,12 +28,14 @@ describe('AuthorizedCollectionSelectorComponent', () => {
|
|||||||
collectionService = jasmine.createSpyObj('collectionService', {
|
collectionService = jasmine.createSpyObj('collectionService', {
|
||||||
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
|
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
|
||||||
});
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [AuthorizedCollectionSelectorComponent, VarDirective],
|
declarations: [AuthorizedCollectionSelectorComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: {} },
|
{ provide: SearchService, useValue: {} },
|
||||||
{ provide: CollectionDataService, useValue: collectionService }
|
{ provide: CollectionDataService, useValue: collectionService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -45,10 +50,10 @@ describe('AuthorizedCollectionSelectorComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
|
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
|
||||||
component.search('', 1).subscribe((result) => {
|
component.search('', 1).subscribe((resultRD) => {
|
||||||
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
|
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
|
||||||
expect(result.page.length).toEqual(1);
|
expect(resultRD.payload.page.length).toEqual(1);
|
||||||
expect(result.page[0].indexableObject).toEqual(collection);
|
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,13 +3,17 @@ import { DSOSelectorComponent } from '../dso-selector.component';
|
|||||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
import { CollectionDataService } from '../../../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../../../core/data/collection-data.service';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||||
import { SearchResult } from '../../../search/search-result.model';
|
import { SearchResult } from '../../../search/search-result.model';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { hasValue } from '../../../empty.util';
|
||||||
|
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-authorized-collection-selector',
|
selector: 'ds-authorized-collection-selector',
|
||||||
@@ -21,8 +25,10 @@ import { followLink } from '../../../utils/follow-link-config.model';
|
|||||||
*/
|
*/
|
||||||
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
|
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected collectionDataService: CollectionDataService) {
|
protected collectionDataService: CollectionDataService,
|
||||||
super(searchService);
|
protected notifcationsService: NotificationsService,
|
||||||
|
protected translate: TranslateService) {
|
||||||
|
super(searchService, notifcationsService, translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,13 +43,15 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
|
|||||||
* @param query Query to search objects for
|
* @param query Query to search objects for
|
||||||
* @param page Page to retrieve
|
* @param page Page to retrieve
|
||||||
*/
|
*/
|
||||||
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> {
|
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({
|
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
elementsPerPage: this.defaultPagination.pageSize
|
elementsPerPage: this.defaultPagination.pageSize
|
||||||
}),true, false, followLink('parentCommunity')).pipe(
|
}),true, false, followLink('parentCommunity')).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstCompletedRemoteData(),
|
||||||
map((list) => buildPaginatedList(list.pageInfo, list.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))))
|
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
|
||||||
|
payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))) : null,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,11 @@
|
|||||||
<div
|
<div
|
||||||
infiniteScroll
|
infiniteScroll
|
||||||
[infiniteScrollDistance]="1"
|
[infiniteScrollDistance]="1"
|
||||||
[infiniteScrollThrottle]="300"
|
[infiniteScrollThrottle]="0"
|
||||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
[infiniteScrollContainer]="'.scrollable-menu'"
|
||||||
[fromRoot]="true"
|
[fromRoot]="true"
|
||||||
(scrolled)="onScrollDown()">
|
(scrolled)="onScrollDown()">
|
||||||
|
<ng-container *ngIf="listEntries">
|
||||||
<button class="list-group-item list-group-item-action border-0 disabled"
|
<button class="list-group-item list-group-item-action border-0 disabled"
|
||||||
*ngIf="listEntries.length == 0">
|
*ngIf="listEntries.length == 0">
|
||||||
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
|
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
|
||||||
[linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader>
|
[linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="hasNextPage"
|
</ng-container>
|
||||||
|
<button *ngIf="loading"
|
||||||
class="list-group-item list-group-item-action border-0 list-entry">
|
class="list-group-item list-group-item-action border-0 list-entry">
|
||||||
<ds-loading [showMessage]="false"></ds-loading>
|
<ds-loading [showMessage]="false"></ds-loading>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -6,10 +6,11 @@ import { SearchService } from '../../../core/shared/search/search.service';
|
|||||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||||
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
|
||||||
import { hasValue } from '../../empty.util';
|
import { hasValue } from '../../empty.util';
|
||||||
import { createPaginatedList } from '../../testing/utils.test';
|
import { createPaginatedList } from '../../testing/utils.test';
|
||||||
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
|
|
||||||
describe('DSOSelectorComponent', () => {
|
describe('DSOSelectorComponent', () => {
|
||||||
let component: DSOSelectorComponent;
|
let component: DSOSelectorComponent;
|
||||||
@@ -59,12 +60,17 @@ describe('DSOSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [DSOSelectorComponent],
|
declarations: [DSOSelectorComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchService },
|
{ provide: SearchService, useValue: searchService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -104,4 +110,15 @@ describe('DSOSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when search returns an error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(searchService, 'search').and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
component.ngOnInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error notification', () => {
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -27,10 +27,13 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
|
|||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||||
import { Context } from '../../../core/shared/context.model';
|
import { Context } from '../../../core/shared/context.model';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../empty.util';
|
||||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { SearchResult } from '../../search/search-result.model';
|
import { SearchResult } from '../../search/search-result.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dso-selector',
|
selector: 'ds-dso-selector',
|
||||||
@@ -78,7 +81,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List with search results of DSpace objects for the current query
|
* List with search results of DSpace objects for the current query
|
||||||
*/
|
*/
|
||||||
listEntries: SearchResult<DSpaceObject>[] = [];
|
listEntries: SearchResult<DSpaceObject>[] = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current page to load
|
* The current page to load
|
||||||
@@ -93,9 +96,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
hasNextPage = false;
|
hasNextPage = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the list should be reset next time it receives a page to load
|
* Whether or not new results are currently loading
|
||||||
*/
|
*/
|
||||||
resetList = false;
|
loading = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of element references to all elements
|
* List of element references to all elements
|
||||||
@@ -123,7 +126,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
public subs: Subscription[] = [];
|
public subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(protected searchService: SearchService) {
|
constructor(protected searchService: SearchService,
|
||||||
|
protected notifcationsService: NotificationsService,
|
||||||
|
protected translate: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,7 +141,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
// Create an observable searching for the current DSO (return empty list if there's no current DSO)
|
// Create an observable searching for the current DSO (return empty list if there's no current DSO)
|
||||||
let currentDSOResult$;
|
let currentDSOResult$;
|
||||||
if (isNotEmpty(this.currentDSOId)) {
|
if (isNotEmpty(this.currentDSOId)) {
|
||||||
currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1);
|
currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
} else {
|
} else {
|
||||||
currentDSOResult$ = observableOf(buildPaginatedList(undefined, []));
|
currentDSOResult$ = observableOf(buildPaginatedList(undefined, []));
|
||||||
}
|
}
|
||||||
@@ -152,31 +157,41 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
this.currentPage$
|
this.currentPage$
|
||||||
).pipe(
|
).pipe(
|
||||||
switchMap(([currentDSOResult, query, page]: [PaginatedList<SearchResult<DSpaceObject>>, string, number]) => {
|
switchMap(([currentDSOResult, query, page]: [PaginatedList<SearchResult<DSpaceObject>>, string, number]) => {
|
||||||
|
this.loading = true;
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
// The first page is loading, this means we should reset the list instead of adding to it
|
// The first page is loading, this means we should reset the list instead of adding to it
|
||||||
this.resetList = true;
|
this.listEntries = null;
|
||||||
}
|
}
|
||||||
return this.search(query, page).pipe(
|
return this.search(query, page).pipe(
|
||||||
map((list) => {
|
map((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
// If it's the first page and no query is entered, add the current DSO to the start of the list
|
// If it's the first page and no query is entered, add the current DSO to the start of the list
|
||||||
// If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already
|
// If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already
|
||||||
list.page = [
|
rd.payload.page = [
|
||||||
...((isEmpty(query) && page === 1) ? currentDSOResult.page : []),
|
...((isEmpty(query) && page === 1) ? currentDSOResult.page : []),
|
||||||
...list.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId)
|
...rd.payload.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId)
|
||||||
];
|
];
|
||||||
return list;
|
} else if (rd.hasFailed) {
|
||||||
|
this.notifcationsService.error(this.translate.instant('dso-selector.error.title', { type: this.typesString }), rd.errorMessage);
|
||||||
|
}
|
||||||
|
return rd;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
).subscribe((list) => {
|
).subscribe((rd) => {
|
||||||
if (this.resetList) {
|
this.loading = false;
|
||||||
this.listEntries = list.page;
|
if (rd.hasSucceeded) {
|
||||||
this.resetList = false;
|
if (hasNoValue(this.listEntries)) {
|
||||||
|
this.listEntries = rd.payload.page;
|
||||||
} else {
|
} else {
|
||||||
this.listEntries.push(...list.page);
|
this.listEntries.push(...rd.payload.page);
|
||||||
}
|
}
|
||||||
// Check if there are more pages available after the current one
|
// Check if there are more pages available after the current one
|
||||||
this.hasNextPage = list.totalElements > this.listEntries.length;
|
this.hasNextPage = rd.payload.totalElements > this.listEntries.length;
|
||||||
|
} else {
|
||||||
|
this.listEntries = null;
|
||||||
|
this.hasNextPage = false;
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +207,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
* @param query Query to search objects for
|
* @param query Query to search objects for
|
||||||
* @param page Page to retrieve
|
* @param page Page to retrieve
|
||||||
*/
|
*/
|
||||||
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> {
|
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
return this.searchService.search(
|
return this.searchService.search(
|
||||||
new PaginatedSearchOptions({
|
new PaginatedSearchOptions({
|
||||||
query: query,
|
query: query,
|
||||||
@@ -202,7 +217,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
).pipe(
|
).pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getFirstCompletedRemoteData()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +225,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
|
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
|
||||||
*/
|
*/
|
||||||
onScrollDown() {
|
onScrollDown() {
|
||||||
if (this.hasNextPage) {
|
if (this.hasNextPage && !this.loading) {
|
||||||
this.currentPage$.next(this.currentPage$.value + 1);
|
this.currentPage$.next(this.currentPage$.value + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,8 +21,6 @@ import { createPaginatedList } from '../../../../testing/utils.test';
|
|||||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
|
||||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
|
||||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
import { Collection } from '../../../../../core/shared/collection.model';
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
|
||||||
@@ -46,8 +44,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
let lookupRelationService;
|
let lookupRelationService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
let submissionId;
|
let submissionId;
|
||||||
let submissionService;
|
|
||||||
let submissionObjectDataService;
|
|
||||||
|
|
||||||
const externalSources = [
|
const externalSources = [
|
||||||
Object.assign(new ExternalSource(), {
|
Object.assign(new ExternalSource(), {
|
||||||
@@ -99,12 +95,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
||||||
});
|
});
|
||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
|
||||||
dispatchSave: jasmine.createSpy('dispatchSave')
|
|
||||||
});
|
|
||||||
submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', {
|
|
||||||
findById: createSuccessfulRemoteDataObject$(testWSI)
|
|
||||||
});
|
|
||||||
submissionId = '1234';
|
submissionId = '1234';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,8 +119,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
},
|
},
|
||||||
{ provide: RelationshipTypeService, useValue: {} },
|
{ provide: RelationshipTypeService, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||||
{ provide: SubmissionService, useValue: submissionService },
|
|
||||||
{ provide: SubmissionObjectDataService, useValue: submissionObjectDataService },
|
|
||||||
{
|
{
|
||||||
provide: Store, useValue: {
|
provide: Store, useValue: {
|
||||||
// tslint:disable-next-line:no-empty
|
// tslint:disable-next-line:no-empty
|
||||||
|
@@ -12,11 +12,8 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
|
|||||||
import { SearchResult } from '../../../../search/search-result.model';
|
import { SearchResult } from '../../../../search/search-result.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction,
|
||||||
getAllSucceededRemoteDataPayload,
|
} from './relationship.actions';
|
||||||
getRemoteDataPayload
|
|
||||||
} from '../../../../../core/shared/operators';
|
|
||||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions';
|
|
||||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -27,12 +24,7 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model
|
|||||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
|
|
||||||
import { Collection } from '../../../../../core/shared/collection.model';
|
|
||||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
|
||||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
|
||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-lookup-relation-modal',
|
selector: 'ds-dynamic-lookup-relation-modal',
|
||||||
@@ -122,10 +114,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
totalExternal$: Observable<number[]>;
|
totalExternal$: Observable<number[]>;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of subscriptions to unsubscribe from
|
|
||||||
*/
|
|
||||||
private subs: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public modal: NgbActiveModal,
|
public modal: NgbActiveModal,
|
||||||
@@ -136,17 +124,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
private lookupRelationService: LookupRelationService,
|
private lookupRelationService: LookupRelationService,
|
||||||
private searchConfigService: SearchConfigurationService,
|
private searchConfigService: SearchConfigurationService,
|
||||||
private rdbService: RemoteDataBuildService,
|
private rdbService: RemoteDataBuildService,
|
||||||
private submissionService: SubmissionService,
|
|
||||||
private submissionObjectService: SubmissionObjectDataService,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private store: Store<AppState>,
|
private store: Store<AppState>,
|
||||||
private router: Router
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setItem();
|
|
||||||
this.selection$ = this.selectableListService
|
this.selection$ = this.selectableListService
|
||||||
.getSelectableList(this.listId)
|
.getSelectableList(this.listId)
|
||||||
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
||||||
@@ -206,24 +191,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize this.item$ based on this.model.submissionId
|
|
||||||
*/
|
|
||||||
private setItem() {
|
|
||||||
const submissionObject$ = this.submissionObjectService
|
|
||||||
.findById(this.submissionId, true, true, followLink('item'), followLink('collection')).pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload()
|
|
||||||
);
|
|
||||||
|
|
||||||
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
|
||||||
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
|
||||||
|
|
||||||
this.subs.push(item$.subscribe((item) => this.item = item));
|
|
||||||
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a subscription updating relationships with name variants
|
* Add a subscription updating relationships with name variants
|
||||||
* @param sri The search result to track name variants for
|
* @param sri The search result to track name variants for
|
||||||
@@ -279,8 +246,5 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.router.navigate([], {});
|
this.router.navigate([], {});
|
||||||
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
||||||
this.subs
|
|
||||||
.filter((sub) => hasValue(sub))
|
|
||||||
.forEach((sub) => sub.unsubscribe());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,12 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
let options: PaginatedSearchOptions;
|
let options: PaginatedSearchOptions;
|
||||||
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
|
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
|
||||||
const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 });
|
const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 });
|
||||||
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
|
const filters = [
|
||||||
|
new SearchFilter('f.test', ['value']),
|
||||||
|
new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded
|
||||||
|
new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not
|
||||||
|
];
|
||||||
|
const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded
|
||||||
const query = 'search query';
|
const query = 'search query';
|
||||||
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||||
const baseUrl = 'www.rest.com';
|
const baseUrl = 'www.rest.com';
|
||||||
@@ -19,7 +24,8 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
filters: filters,
|
filters: filters,
|
||||||
query: query,
|
query: query,
|
||||||
scope: scope,
|
scope: scope,
|
||||||
dsoTypes: [DSpaceObjectType.ITEM]
|
dsoTypes: [DSpaceObjectType.ITEM],
|
||||||
|
fixedFilter: fixedFilter,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,12 +37,14 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
'sort=test.field,DESC&' +
|
'sort=test.field,DESC&' +
|
||||||
'page=0&' +
|
'page=0&' +
|
||||||
'size=40&' +
|
'size=40&' +
|
||||||
'query=search query&' +
|
'f.fixed=1234%2C5678,equals&' +
|
||||||
|
'query=search%20query&' +
|
||||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||||
'dsoType=ITEM&' +
|
'dsoType=ITEM&' +
|
||||||
'f.test=value&' +
|
'f.test=value&' +
|
||||||
'f.example=another value&' +
|
'f.example=another%20value&' +
|
||||||
'f.example=second value'
|
'f.example=second%20value&' +
|
||||||
|
'f.range=%5B2002%20TO%202021%5D,equals'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* True when the filter is 100% collapsed in the UI
|
||||||
*/
|
*/
|
||||||
closed = true;
|
closed: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the filter is currently collapsed in the store
|
* Emits true when the filter is currently collapsed in the store
|
||||||
|
@@ -8,18 +8,16 @@
|
|||||||
|
|
||||||
::ng-deep
|
::ng-deep
|
||||||
{
|
{
|
||||||
--ds-slider-handle-width: 18px;
|
|
||||||
|
|
||||||
html:not([dir=rtl]) .noUi-horizontal .noUi-handle {
|
html:not([dir=rtl]) .noUi-horizontal .noUi-handle {
|
||||||
right: calc(var(--ds-slider-handle-width) / -2);
|
right: calc(var(--ds-slider-handle-width) / -2);
|
||||||
}
|
}
|
||||||
.noUi-horizontal .noUi-handle {
|
.noUi-horizontal .noUi-handle {
|
||||||
width: var(--ds-slider-handle-width);
|
width: var(--ds-slider-handle-width);
|
||||||
&:before {
|
&:before {
|
||||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) - 2);
|
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) - 2px);
|
||||||
}
|
}
|
||||||
&:after {
|
&:after {
|
||||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) + 2);
|
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) + 2px);
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@@ -56,7 +56,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
/**
|
/**
|
||||||
* Fallback maximum for the range
|
* Fallback maximum for the range
|
||||||
*/
|
*/
|
||||||
max = 2018;
|
max = new Date().getFullYear();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current range of the filter
|
* The current range of the filter
|
||||||
|
@@ -4,13 +4,25 @@ import { SearchFilter } from './search-filter.model';
|
|||||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
|
|
||||||
describe('SearchOptions', () => {
|
describe('SearchOptions', () => {
|
||||||
let options: PaginatedSearchOptions;
|
let options: SearchOptions;
|
||||||
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
|
|
||||||
|
const filters = [
|
||||||
|
new SearchFilter('f.test', ['value']),
|
||||||
|
new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded
|
||||||
|
new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not
|
||||||
|
];
|
||||||
|
const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded
|
||||||
const query = 'search query';
|
const query = 'search query';
|
||||||
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||||
const baseUrl = 'www.rest.com';
|
const baseUrl = 'www.rest.com';
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
options = new SearchOptions({ filters: filters, query: query, scope: scope, dsoTypes: [DSpaceObjectType.ITEM] });
|
options = new SearchOptions({
|
||||||
|
filters: filters,
|
||||||
|
query: query,
|
||||||
|
scope: scope,
|
||||||
|
dsoTypes: [DSpaceObjectType.ITEM],
|
||||||
|
fixedFilter: fixedFilter,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when toRestUrl is called', () => {
|
describe('when toRestUrl is called', () => {
|
||||||
@@ -18,12 +30,14 @@ describe('SearchOptions', () => {
|
|||||||
it('should generate a string with all parameters that are present', () => {
|
it('should generate a string with all parameters that are present', () => {
|
||||||
const outcome = options.toRestUrl(baseUrl);
|
const outcome = options.toRestUrl(baseUrl);
|
||||||
expect(outcome).toEqual('www.rest.com?' +
|
expect(outcome).toEqual('www.rest.com?' +
|
||||||
'query=search query&' +
|
'f.fixed=1234%2C5678,equals&' +
|
||||||
|
'query=search%20query&' +
|
||||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||||
'dsoType=ITEM&' +
|
'dsoType=ITEM&' +
|
||||||
'f.test=value&' +
|
'f.test=value&' +
|
||||||
'f.example=another value&' +
|
'f.example=another%20value&' +
|
||||||
'f.example=second value'
|
'f.example=second%20value&' +
|
||||||
|
'f.range=%5B2002%20TO%202021%5D,equals'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { SearchFilter } from './search-filter.model';
|
import { SearchFilter } from './search-filter.model';
|
||||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
@@ -13,10 +13,15 @@ export class SearchOptions {
|
|||||||
scope?: string;
|
scope?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
dsoTypes?: DSpaceObjectType[];
|
dsoTypes?: DSpaceObjectType[];
|
||||||
filters?: any;
|
filters?: SearchFilter[];
|
||||||
fixedFilter?: any;
|
fixedFilter?: string;
|
||||||
|
|
||||||
constructor(options: {configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[], fixedFilter?: any}) {
|
constructor(
|
||||||
|
options: {
|
||||||
|
configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[],
|
||||||
|
fixedFilter?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
this.configuration = options.configuration;
|
this.configuration = options.configuration;
|
||||||
this.scope = options.scope;
|
this.scope = options.scope;
|
||||||
this.query = options.query;
|
this.query = options.query;
|
||||||
@@ -33,27 +38,27 @@ export class SearchOptions {
|
|||||||
*/
|
*/
|
||||||
toRestUrl(url: string, args: string[] = []): string {
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
if (isNotEmpty(this.configuration)) {
|
if (isNotEmpty(this.configuration)) {
|
||||||
args.push(`configuration=${this.configuration}`);
|
args.push(`configuration=${encodeURIComponent(this.configuration)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.fixedFilter)) {
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
args.push(this.fixedFilter);
|
args.push(this.encodedFixedFilter);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.query)) {
|
if (isNotEmpty(this.query)) {
|
||||||
args.push(`query=${this.query}`);
|
args.push(`query=${encodeURIComponent(this.query)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.scope)) {
|
if (isNotEmpty(this.scope)) {
|
||||||
args.push(`scope=${this.scope}`);
|
args.push(`scope=${encodeURIComponent(this.scope)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.dsoTypes)) {
|
if (isNotEmpty(this.dsoTypes)) {
|
||||||
this.dsoTypes.forEach((dsoType: string) => {
|
this.dsoTypes.forEach((dsoType: string) => {
|
||||||
args.push(`dsoType=${dsoType}`);
|
args.push(`dsoType=${encodeURIComponent(dsoType)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.filters)) {
|
if (isNotEmpty(this.filters)) {
|
||||||
this.filters.forEach((filter: SearchFilter) => {
|
this.filters.forEach((filter: SearchFilter) => {
|
||||||
filter.values.forEach((value) => {
|
filter.values.forEach((value) => {
|
||||||
const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : '');
|
const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : '');
|
||||||
args.push(`${filter.key}=${filterValue}`);
|
args.push(`${filter.key}=${this.encodeFilterQueryValue(filterValue)}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -62,4 +67,28 @@ export class SearchOptions {
|
|||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get encodedFixedFilter(): string {
|
||||||
|
// expected format: 'arg=value'
|
||||||
|
// -> split the query agument into (arg=)(value) and only encode 'value'
|
||||||
|
const match = this.fixedFilter.match(/^([^=]+=)(.+)$/);
|
||||||
|
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return match[1] + this.encodeFilterQueryValue(match[2]);
|
||||||
|
} else {
|
||||||
|
return this.encodeFilterQueryValue(this.fixedFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeFilterQueryValue(filterQueryValue: string): string {
|
||||||
|
// expected format: 'value' or 'value,operator'
|
||||||
|
// -> split into (value)(,operator) and only encode 'value'
|
||||||
|
const match = filterQueryValue.match(/^(.*)(,\w+)$/);
|
||||||
|
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return encodeURIComponent(match[1]) + match[2];
|
||||||
|
} else {
|
||||||
|
return encodeURIComponent(filterQueryValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ng-container *ngVar="(searchOptions$ | async) as config">
|
<ng-container *ngVar="searchOptions as config">
|
||||||
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
||||||
<div class="result-order-settings">
|
<div class="result-order-settings">
|
||||||
<ds-sidebar-dropdown
|
<ds-sidebar-dropdown
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
[label]="'search.sidebar.settings.sort-by'"
|
[label]="'search.sidebar.settings.sort-by'"
|
||||||
(change)="reloadOrder($event)"
|
(change)="reloadOrder($event)"
|
||||||
>
|
>
|
||||||
<option *ngFor="let sortOption of searchOptionPossibilities"
|
<option *ngFor="let sortOption of sortOptions"
|
||||||
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
||||||
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
||||||
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
||||||
|
@@ -12,7 +12,6 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||||
import { VarDirective } from '../../utils/var.directive';
|
import { VarDirective } from '../../utils/var.directive';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
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 { SidebarService } from '../../sidebar/sidebar.service';
|
import { SidebarService } from '../../sidebar/sidebar.service';
|
||||||
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
||||||
@@ -91,7 +90,7 @@ describe('SearchSettingsComponent', () => {
|
|||||||
provide: SEARCH_CONFIG_SERVICE,
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
useValue: {
|
useValue: {
|
||||||
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
||||||
getCurrentScope: observableOf('test-id')
|
getCurrentScope: observableOf('test-id'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -103,6 +102,14 @@ describe('SearchSettingsComponent', () => {
|
|||||||
fixture = TestBed.createComponent(SearchSettingsComponent);
|
fixture = TestBed.createComponent(SearchSettingsComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
comp.sortOptions = [
|
||||||
|
new SortOptions('score', SortDirection.DESC),
|
||||||
|
new SortOptions('dc.title', SortDirection.ASC),
|
||||||
|
new SortOptions('dc.title', SortDirection.DESC)
|
||||||
|
];
|
||||||
|
|
||||||
|
comp.searchOptions = paginatedSearchOptions;
|
||||||
|
|
||||||
// SearchPageComponent test instance
|
// SearchPageComponent test instance
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
searchServiceObject = (comp as any).service;
|
searchServiceObject = (comp as any).service;
|
||||||
@@ -111,34 +118,24 @@ describe('SearchSettingsComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should show the order settings with the respective selectable options', (done) => {
|
it('it should show the order settings with the respective selectable options', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
expect(orderSetting).toBeDefined();
|
expect(orderSetting).toBeDefined();
|
||||||
const childElements = orderSetting.queryAll(By.css('option'));
|
const childElements = orderSetting.queryAll(By.css('option'));
|
||||||
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
|
expect(childElements.length).toEqual(comp.sortOptions.length);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should show the size settings', (done) => {
|
it('it should show the size settings', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
||||||
expect(pageSizeSetting).toBeDefined();
|
expect(pageSizeSetting).toBeDefined();
|
||||||
done();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have the proper order value selected by default', (done) => {
|
it('should have the proper order value selected by default', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
|
const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]'));
|
||||||
expect(childElementToBeSelected).toBeDefined();
|
expect(childElementToBeSelected).toBeDefined();
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, Input } from '@angular/core';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.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 { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
@@ -17,16 +16,17 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
/**
|
/**
|
||||||
* This component represents the part of the search sidebar that contains the general search settings.
|
* This component represents the part of the search sidebar that contains the general search settings.
|
||||||
*/
|
*/
|
||||||
export class SearchSettingsComponent implements OnInit {
|
export class SearchSettingsComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration for the current paginated search results
|
* The configuration for the current paginated search results
|
||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
@Input() searchOptions: PaginatedSearchOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All sort options that are shown in the settings
|
* All sort options that are shown in the settings
|
||||||
*/
|
*/
|
||||||
searchOptionPossibilities = [new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC)];
|
@Input() sortOptions: SortOptions[];
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -35,13 +35,6 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize paginated search options
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.searchOptions$ = this.searchConfigurationService.paginatedSearchOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to change the current sort field and direction
|
* Method to change the current sort field and direction
|
||||||
* @param {Event} event Change event containing the sort direction and sort field
|
* @param {Event} event Change event containing the sort direction and sort field
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
|
<ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
|
||||||
<ds-search-filters [refreshFilters]="refreshFilters" [inPlaceSearch]="inPlaceSearch"></ds-search-filters>
|
<ds-search-filters [refreshFilters]="refreshFilters" [inPlaceSearch]="inPlaceSearch"></ds-search-filters>
|
||||||
<ds-search-settings></ds-search-settings>
|
<ds-search-settings [searchOptions]="searchOptions" [sortOptions]="sortOptions"></ds-search-settings>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
|
|
||||||
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
|
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
|
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -45,6 +47,16 @@ export class SearchSidebarComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() inPlaceSearch;
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the current paginated search results
|
||||||
|
*/
|
||||||
|
@Input() searchOptions: PaginatedSearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All sort options that are shown in the settings
|
||||||
|
*/
|
||||||
|
@Input() sortOptions: SortOptions[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits when the search filters values may be stale, and so they must be refreshed.
|
* Emits when the search filters values may be stale, and so they must be refreshed.
|
||||||
*/
|
*/
|
||||||
|
@@ -1168,6 +1168,8 @@
|
|||||||
|
|
||||||
"dso-selector.edit.item.head": "Edit item",
|
"dso-selector.edit.item.head": "Edit item",
|
||||||
|
|
||||||
|
"dso-selector.error.title": "An error occurred searching for a {{ type }}",
|
||||||
|
|
||||||
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
|
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
|
||||||
|
|
||||||
"dso-selector.no-results": "No {{ type }} found",
|
"dso-selector.no-results": "No {{ type }} found",
|
||||||
@@ -3010,6 +3012,8 @@
|
|||||||
"search.results.empty": "Your search returned no results.",
|
"search.results.empty": "Your search returned no results.",
|
||||||
|
|
||||||
|
|
||||||
|
"default.search.results.head": "Search Results",
|
||||||
|
|
||||||
|
|
||||||
"search.sidebar.close": "Back to results",
|
"search.sidebar.close": "Back to results",
|
||||||
|
|
||||||
@@ -3043,8 +3047,21 @@
|
|||||||
|
|
||||||
"sorting.dc.title.DESC": "Title Descending",
|
"sorting.dc.title.DESC": "Title Descending",
|
||||||
|
|
||||||
"sorting.score.DESC": "Relevance",
|
"sorting.score.ASC": "Least Relevant",
|
||||||
|
|
||||||
|
"sorting.score.DESC": "Most Relevant",
|
||||||
|
|
||||||
|
"sorting.dc.date.issued.ASC": "Date Issued Ascending",
|
||||||
|
|
||||||
|
"sorting.dc.date.issued.DESC": "Date Issued Descending",
|
||||||
|
|
||||||
|
"sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending",
|
||||||
|
|
||||||
|
"sorting.dc.date.accessioned.DESC": "Accessioned Date Descending",
|
||||||
|
|
||||||
|
"sorting.lastModified.ASC": "Last modified Ascending",
|
||||||
|
|
||||||
|
"sorting.lastModified.DESC": "Last modified Descending",
|
||||||
|
|
||||||
|
|
||||||
"statistics.title": "Statistics",
|
"statistics.title": "Statistics",
|
||||||
|
@@ -78,4 +78,5 @@
|
|||||||
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
|
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
|
||||||
|
|
||||||
--ds-slider-color: #{$green};
|
--ds-slider-color: #{$green};
|
||||||
|
--ds-slider-handle-width: 18px;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user