Intermediate commit

This commit is contained in:
Giuseppe Digilio
2019-03-08 19:49:07 +01:00
parent f0a6d7ac3d
commit 52906e71c4
40 changed files with 845 additions and 140 deletions

View File

@@ -236,6 +236,7 @@
},
"login": "Log In",
"logout": "Log Out",
"mydspace": "MyDSpace",
"language": "Language switch",
"search": "Search"
},
@@ -272,12 +273,62 @@
"help": "Select a community to browse its collections."
}
},
"mydspace": {
"title": "MyDSpace",
"description": "",
"new-submission-btn": "New submission",
"results": {
"head": "Your submissions",
"no-results": "There were no items to show",
"no-title": "No title",
"no-authors": "No Authors",
"no-date": "No Date",
"no-abstract": "No Abstract"
},
"messages": {
"title": "Messages",
"to": "To",
"hide-msg": "Hide message",
"show-msg": "Show message",
"no-messages": "No messages yet.",
"no-content": "No content.",
"send-btn": "Send",
"subject-placeholder": "Subject...",
"description-placeholder": "Insert your message here...",
"mark-as-read": "Mark as read",
"mark-as-unread": "Mark as unread",
"submitter-help": "Select this option to send a message to controller.",
"controller-help": "Select this option to send a message to item's submitter."
},
"show": {
"workspace": "Your Submissions",
"workflow": "All tasks"
},
"status": {
"workflow": "Under review",
"rejected": "Rejected",
"validation": "Validate",
"waiting-for-controller": "Claim",
"in-progress": "To do",
"accepted": "Finalized"
},
"view-btn": "View",
"general": {
"text-here": "HERE"
},
"upload": {
"upload-successful": "New workspace item created. Click {{here}} for edit it.",
"upload-multiple-successful": "{{qty}} new workspace items created.",
"upload-failed": "Error creating new workspace. Please verify the content uploaded before retry."
}
},
"search": {
"title": "DSpace Angular :: Search",
"description": "",
"form": {
"search": "Search",
"search_dspace": "Search DSpace"
"search_dspace": "Search DSpace",
"search_mydspace": "Search MyDSpace"
},
"results": {
"head": "Search Results",
@@ -295,11 +346,18 @@
"title": "Settings",
"sort-by": "Sort By",
"rpp": "Results per page"
},
"tab":{
"title":"Show"
}
},
"switch-configuration": {
"title":"Show"
},
"view-switch": {
"show-list": "Show as list",
"show-grid": "Show as grid"
"show-grid": "Show as grid",
"show-detail": "Show detail"
},
"filters": {
"head": "Filters",
@@ -309,7 +367,10 @@
"f.dateIssued.min": "Start date",
"f.dateIssued.max": "End date",
"f.subject": "Subject",
"f.has_content_in_original_bundle": "Has files"
"f.namedresourcetype": "Status",
"f.dateSubmitted": "Date submitted",
"f.itemtype": "Type",
"f.submitter": "Submitter"
},
"filter": {
"show-more": "Show more",
@@ -337,6 +398,26 @@
},
"has_content_in_original_bundle": {
"head": "Has files"
},
"namedresourcetype": {
"placeholder": "Status",
"head": "Status"
},
"dateSubmitted": {
"placeholder": "Date submitted",
"head": "Date submitted"
},
"itemtype": {
"placeholder": "Type",
"head": "Type"
},
"submitter": {
"placeholder": "Submitter",
"head": "Submitter"
},
"objectpeople": {
"placeholder": "People",
"head": "People"
}
}
}
@@ -532,6 +613,7 @@
"item": "Loading item...",
"objects": "Loading...",
"search-results": "Loading search results...",
"mydspace-results": "Loading items...",
"browse-by": "Loading items..."
},
"error": {
@@ -692,6 +774,49 @@
}
}
}
},
"workflow": {
"generic": {
"delete": "Delete",
"delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.",
"edit": "Edit",
"edit-help": "Select this option to change the item's metadata.",
"view": "View",
"view-help": "Select this option to view the item's metadata."
},
"tasks": {
"generic": {
"processing": "Processing...",
"success": "Operation successful",
"error": "Error occurred during operation...",
"submitter": "Submitter"
},
"claimed": {
"approve": "Approve",
"approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".",
"edit": "Edit",
"edit_help": "Select this option to change the item's metadata.",
"reject": {
"submit": "Reject",
"reason": {
"submit": "Reject item",
"title": "Reason",
"info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.",
"placeholder": "Describe the reason of reject"
}
},
"reject_help": "If you have reviewed the item and found it is <strong>not</strong> suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.",
"return": "Return to pool",
"return_help": "Return the task to the pool so that another user may perform the task."
},
"pool": {
"claim": "Claim",
"claim_help": "Assign this task to yourself.",
"show-detail": "Show detail",
"hide-detail": "Hide detail"
}
}
}
},
"uploader": {

View File

@@ -5,17 +5,6 @@ import { SharedModule } from '../shared/shared.module';
import { ItemPageComponent } from './simple/item-page.component';
import { ItemPageRoutingModule } from './item-page-routing.module';
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
import { ItemPageSpecificFieldComponent } from './simple/field-components/specific-field/item-page-specific-field.component';
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
import { CollectionsComponent } from './field-components/collections/collections.component';
import { FullItemPageComponent } from './full/full-item-page.component';
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
@@ -30,17 +19,6 @@ import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
declarations: [
ItemPageComponent,
FullItemPageComponent,
MetadataValuesComponent,
MetadataUriValuesComponent,
MetadataFieldWrapperComponent,
ItemPageAuthorFieldComponent,
ItemPageDateFieldComponent,
ItemPageAbstractFieldComponent,
ItemPageUriFieldComponent,
ItemPageTitleFieldComponent,
ItemPageSpecificFieldComponent,
FileSectionComponent,
CollectionsComponent,
FullFileSectionComponent
]
})

View File

@@ -51,6 +51,6 @@ export class ItemPageComponent implements OnInit {
this.thumbnail$ = this.itemRD$.pipe(
map((rd: RemoteData<Item>) => rd.payload),
filter((item: Item) => hasValue(item)),
mergeMap((item: Item) => item.getThumbnail()),);
mergeMap((item: Item) => item.getThumbnail()));
}
}

View File

@@ -1,25 +1,67 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
import { RoleService } from '../core/roles/role.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { RouteService } from '../shared/services/route.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
/**
* Service that performs all actions that have to do with the current search configuration
*/
@Injectable()
export class MyDSpaceConfigurationService {
export class MyDSpaceConfigurationService extends SearchConfigurationService {
/**
* Default pagination settings
*/
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
id: 'mydspace-page-configuration',
pageSize: 10,
currentPage: 1
});
/**
* Default sort settings
*/
protected defaultSort = new SortOptions('dc.date.issued', SortDirection.DESC);
/**
* Default configuration parameter setting
*/
protected defaultConfiguration = 'default';
/**
* Default scope setting
*/
protected defaultScope = '';
/**
* Default query setting
*/
protected defaultQuery = '';
private isAdmin$: Observable<boolean>;
private isController$: Observable<boolean>;
private isSubmitter$: Observable<boolean>;
/**
* @constructor
* Initialize class
*
* @param {roleService} roleService
* @param {RouteService} routeService
* @param {ActivatedRoute} route
*/
constructor(protected roleService: RoleService) {
constructor(protected roleService: RoleService,
protected routeService: RouteService,
protected route: ActivatedRoute) {
super(routeService, route);
this.isSubmitter$ = this.roleService.isSubmitter();
this.isController$ = this.roleService.isController();
this.isAdmin$ = this.roleService.isAdmin();

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map, switchMap, tap, } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, Inject, InjectionToken, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { switchMap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model';
@@ -18,9 +18,9 @@ import { SearchConfigurationOption } from '../+search-page/search-switch-configu
import { RoleType } from '../core/roles/role-types';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { SearchFilterConfig } from '../+search-page/search-service/search-filter-config.model';
export const MYDSPACE_ROUTE = '/mydspace';
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
/**
* This component renders a simple item page.
@@ -33,7 +33,13 @@ export const MYDSPACE_ROUTE = '/mydspace';
styleUrls: ['./my-dspace-page.component.scss'],
templateUrl: './my-dspace-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut]
animations: [pushInOut],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: MyDSpaceConfigurationService
}
]
})
/**
@@ -77,8 +83,7 @@ export class MyDSpacePageComponent implements OnInit {
private sidebarService: SearchSidebarService,
private windowService: HostWindowService,
private filterService: SearchFilterService,
private configurationService: MyDSpaceConfigurationService,
private searchConfigService: SearchConfigurationService) {
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.service.setServiceOptions(MyDSpaceResponseParsingService, true);
}
@@ -91,7 +96,7 @@ export class MyDSpacePageComponent implements OnInit {
* If something changes, update the list of scopes for the dropdown
*/
ngOnInit(): void {
this.configurationList$ = this.configurationService.getAvailableConfigurationOptions();
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$.pipe(

View File

@@ -1,4 +1,4 @@
import { autoserialize } from 'cerialize';
import { autoserialize, autoserializeAs } from 'cerialize';
import { MetadataMap } from '../core/shared/metadata.interfaces';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
@@ -9,7 +9,7 @@ export class NormalizedSearchResult implements ListableObject {
/**
* The UUID of the DSpaceObject that was found
*/
@autoserialize
@autoserializeAs(String, 'rObject')
dspaceObject: string;
/**

View File

@@ -12,7 +12,7 @@ export class PaginatedSearchOptions extends SearchOptions {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], pagination?: PaginationComponentOptions, sort?: SortOptions}) {
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], pagination?: PaginationComponentOptions, sort?: SortOptions}) {
super(options);
this.pagination = options.pagination;
this.sort = options.sort;

View File

@@ -21,6 +21,7 @@ import { SearchService } from '../../../search-service/search.service';
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
@Component({
selector: 'ds-search-facet-filter',
@@ -74,9 +75,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected searchConfigService: SearchConfigurationService,
protected rdbs: RemoteDataBuildService,
protected router: Router,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
}

View File

@@ -1,9 +1,10 @@
import { take } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchFilterService } from './search-filter.service';
import { Observable } from 'rxjs';
import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util';

View File

@@ -22,6 +22,7 @@ import * as moment from 'moment';
import { RouteService } from '../../../../shared/services/route.service';
import { hasValue } from '../../../../shared/empty.util';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
/**
* This component renders a simple item page.
@@ -67,13 +68,13 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected searchConfigService: SearchConfigurationService,
protected router: Router,
protected rdbs: RemoteDataBuildService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
@Inject(PLATFORM_ID) private platformId: any,
private route: RouteService) {
super(searchService, filterService, searchConfigService, rdbs, router, filterConfig);
super(searchService, filterService, rdbs, router, searchConfigService, filterConfig);
}

View File

@@ -1,7 +1,7 @@
<h3>{{"search.filters.head" | translate}}</h3>
<div *ngIf="(filters | async)?.hasSucceeded">
<div *ngFor="let filter of (filters | async)?.payload">
<ds-search-filter *ngIf="isActive(filter) | async" class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
<div *ngIf="!isLoadingFilters$.value">
<div *ngFor="let filter of filters">
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
</div>
</div>
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" [queryParamsHandling]="merge" role="button">{{"search.filters.reset" | translate}}</a>

View File

@@ -1,14 +1,17 @@
import { Observable, of as observableOf } from 'rxjs';
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { filter, first, map, mergeMap, startWith, switchMap, tap } from 'rxjs/operators';
import { filter, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
import { Component } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { RemoteData } from '../../core/data/remote-data';
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { isNotEmpty } from '../../shared/empty.util';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { SearchFilterService } from './search-filter/search-filter.service';
import { getSucceededRemoteData } from '../../core/shared/operators';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
@Component({
selector: 'ds-search-filters',
@@ -19,11 +22,12 @@ import { getSucceededRemoteData } from '../../core/shared/operators';
/**
* This component represents the part of the search sidebar that contains filters.
*/
export class SearchFiltersComponent {
export class SearchFiltersComponent implements OnDestroy, OnInit {
/**
* An observable containing configuration about which filters are shown and how they are shown
* An Array containing configuration about which filters are shown and how they are shown
*/
filters: Observable<RemoteData<SearchFilterConfig[]>>;
filters: SearchFilterConfig[] = [];
/**
* List of all filters that are currently active with their value set to null.
@@ -31,15 +35,44 @@ export class SearchFiltersComponent {
*/
clearParams;
/**
* A boolean representing load state of filters configuration
*/
isLoadingFilters$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
/**
* The current paginated search options
*/
searchOptions$: Observable<PaginatedSearchOptions>;
private sub: Subscription;
/**
* Initialize instance variables
* @param {ChangeDetectorRef} cdr
* @param {SearchService} searchService
* @param {SearchConfigurationService} searchConfigService
* @param {SearchFilterService} filterService
*/
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService, private filterService: SearchFilterService) {
this.filters = searchService.getConfig().pipe(getSucceededRemoteData());
this.clearParams = searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
constructor(
private cdr: ChangeDetectorRef,
private searchService: SearchService,
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService,
private filterService: SearchFilterService) {
}
ngOnInit(): void {
this.searchOptions$ = this.searchConfigService.searchOptions;
this.sub = this.searchOptions$.pipe(
tap(() => this.setLoading()),
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getSucceededRemoteData())))
.subscribe((filtersRD: RemoteData<SearchFilterConfig[]>) => {
this.filters = filtersRD.payload;
this.isLoadingFilters$.next(false);
});
this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
Object.keys(filters).forEach((f) => filters[f] = null);
return filters;
}));
@@ -58,12 +91,13 @@ export class SearchFiltersComponent {
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
*/
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
console.log('isActive', filterConfig);
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
mergeMap((isActive) => {
if (isNotEmpty(isActive)) {
return observableOf(true);
} else {
return this.searchConfigService.searchOptions.pipe(
return this.searchOptions$.pipe(
switchMap((options) => {
return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
filter((RD) => !RD.isLoading),
@@ -73,6 +107,20 @@ export class SearchFiltersComponent {
}
))
}
}),startWith(true),);
}),
first(),
startWith(true),);
}
private setLoading() {
this.isLoadingFilters$.next(true);
this.cdr.detectChanges();
}
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { Observable } from 'rxjs';
import { Params } from '@angular/router';
import { map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
@Component({
selector: 'ds-search-labels',
@@ -24,7 +25,9 @@ export class SearchLabelsComponent {
/**
* Initialize the instance variable
*/
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
constructor(
private searchService: SearchService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters();
}

View File

@@ -8,12 +8,14 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
* This model class represents all parameters needed to request information about a certain search request
*/
export class SearchOptions {
configuration?: string;
scope?: string;
query?: string;
dsoType?: DSpaceObjectType;
filters?: SearchFilter[];
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[]}) {
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[]}) {
this.configuration = options.configuration;
this.scope = options.scope;
this.query = options.query;
this.dsoType = options.dsoType;
@@ -28,6 +30,9 @@ export class SearchOptions {
*/
toRestUrl(url: string, args: string[] = []): string {
if (isNotEmpty(this.configuration)) {
args.push(`configuration=${this.configuration}`);
}
if (isNotEmpty(this.query)) {
args.push(`query=${this.query}`);
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { switchMap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list';
@@ -7,13 +7,13 @@ import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { hasValue } from '../shared/empty.util';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { getSucceededRemoteData } from '../core/shared/operators';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
/**
* This component renders a simple item page.
@@ -26,7 +26,13 @@ import { getSucceededRemoteData } from '../core/shared/operators';
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut]
animations: [pushInOut],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
})
/**
@@ -62,8 +68,7 @@ export class SearchPageComponent implements OnInit {
constructor(private service: SearchService,
private sidebarService: SearchSidebarService,
private windowService: HostWindowService,
private filterService: SearchFilterService,
private searchConfigService: SearchConfigurationService) {
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}

View File

@@ -28,20 +28,13 @@ import { SearchFacetFilterWrapperComponent } from './search-filters/search-filte
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component';
const effects = [
SearchSidebarEffects
];
@NgModule({
imports: [
SearchPageRoutingModule,
CommonModule,
SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot()
],
declarations: [
const components = [
SearchPageComponent,
SearchResultsComponent,
SearchSidebarComponent,
@@ -63,7 +56,18 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchSwitchConfigurationComponent
];
@NgModule({
imports: [
SearchPageRoutingModule,
CommonModule,
SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot()
],
declarations: components,
providers: [
SearchService,
SearchSidebarService,
@@ -82,7 +86,8 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
]
],
exports: components
})
/**

View File

@@ -6,7 +6,13 @@ import { autoserialize, autoserializeAs } from 'cerialize';
*/
export class FacetValue {
/**
* The display value of the facet value
* The display label of the facet value
*/
@autoserialize
label: string;
/**
* The value of the facet value
*/
@autoserializeAs(String, 'label')
value: string;

View File

@@ -1,3 +1,6 @@
import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
@@ -7,12 +10,11 @@ import {
Subscription
} from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SearchOptions } from '../search-options.model';
import { ActivatedRoute, Params } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Injectable, OnDestroy } from '@angular/core';
import { RouteService } from '../../shared/services/route.service';
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from '../../core/data/remote-data';
@@ -28,7 +30,7 @@ export class SearchConfigurationService implements OnDestroy {
/**
* Default pagination settings
*/
private defaultPagination = Object.assign(new PaginationComponentOptions(), {
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
id: 'search-page-configuration',
pageSize: 10,
currentPage: 1
@@ -37,17 +39,22 @@ export class SearchConfigurationService implements OnDestroy {
/**
* Default sort settings
*/
private defaultSort = new SortOptions('score', SortDirection.DESC);
protected defaultSort = new SortOptions('score', SortDirection.DESC);
/**
* Default configuration parameter setting
*/
protected defaultConfiguration = 'default';
/**
* Default scope setting
*/
private defaultScope = '';
protected defaultScope = '';
/**
* Default query setting
*/
private defaultQuery = '';
protected defaultQuery = '';
/**
* Emits the current default values
@@ -74,8 +81,8 @@ export class SearchConfigurationService implements OnDestroy {
* @param {RouteService} routeService
* @param {ActivatedRoute} route
*/
constructor(private routeService: RouteService,
private route: ActivatedRoute) {
constructor(protected routeService: RouteService,
protected route: ActivatedRoute) {
this.defaults
.pipe(getSucceededRemoteData())
.subscribe((defRD) => {
@@ -85,10 +92,20 @@ export class SearchConfigurationService implements OnDestroy {
this.subs.push(this.subscribeToSearchOptions(defs));
this.subs.push(this.subscribeToPaginatedSearchOptions(defs));
}
)
}
/**
* @returns {Observable<string>} Emits the current configuration string
*/
getCurrentConfiguration(defaultConfiguration: string) {
return this.routeService.getQueryParameterValue('configuration').pipe(map((configuration) => {
return configuration || defaultConfiguration;
}));
}
/**
* @returns {Observable<string>} Emits the current scope's identifier
*/
@@ -188,6 +205,7 @@ export class SearchConfigurationService implements OnDestroy {
*/
subscribeToSearchOptions(defaults: SearchOptions): Subscription {
return observableMerge(
this.getConfigurationPart(defaults.configuration),
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
this.getDSOTypePart(),
@@ -208,6 +226,7 @@ export class SearchConfigurationService implements OnDestroy {
return observableMerge(
this.getPaginationPart(defaults.pagination),
this.getSortPart(defaults.sort),
this.getConfigurationPart(defaults.configuration),
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
this.getDSOTypePart(),
@@ -226,6 +245,7 @@ export class SearchConfigurationService implements OnDestroy {
if (hasNoValue(this._defaults)) {
const options = new PaginatedSearchOptions({
pagination: this.defaultPagination,
configuration: this.defaultConfiguration,
sort: this.defaultSort,
scope: this.defaultScope,
query: this.defaultQuery
@@ -242,6 +262,16 @@ export class SearchConfigurationService implements OnDestroy {
this.subs.forEach((sub) => {
sub.unsubscribe();
});
this.subs = [];
}
/**
* @returns {Observable<string>} Emits the current configuration settings as a partial SearchOptions object
*/
private getConfigurationPart(defaultConfiguration: string): Observable<any> {
return this.getCurrentConfiguration(defaultConfiguration).pipe(map((configuration) => {
return { configuration }
}));
}
/**

View File

@@ -34,7 +34,7 @@ export class SearchQueryResponse {
* The sort parameters used in the search request
*/
@autoserialize
configurationName: string;
configuration: string;
/**
* The sort parameters used in the search request

View File

@@ -1,5 +1,6 @@
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { isNull } from '../../shared/empty.util';
/**
* Contains the mapping between a search result component and a DSpaceObject
@@ -11,12 +12,19 @@ const searchResultMap = new Map();
* @param {GenericConstructor<ListableObject>} domainConstructor The constructor of the DSpaceObject
* @returns Decorator function that performs the actual mapping on initialization of the component
*/
export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>, configuration: string = null) {
return function decorator(searchResult: any) {
if (!searchResult) {
return;
}
if (isNull(configuration)) {
searchResultMap.set(domainConstructor, searchResult);
} else {
if (!searchResultMap.get(configuration)) {
searchResultMap.set(configuration, new Map());
}
searchResultMap.get(configuration).set(domainConstructor, searchResult);
}
};
}
@@ -25,6 +33,10 @@ export function searchResultFor(domainConstructor: GenericConstructor<ListableOb
* @param {GenericConstructor<ListableObject>} domainConstructor The DSpaceObject's constructor for which the search result component is requested
* @returns The component's constructor that matches the given DSpaceObject
*/
export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>, configuration: string = null) {
if (isNull(configuration) || configuration === 'default') {
return searchResultMap.get(domainConstructor);
} else {
return searchResultMap.get(configuration).get(domainConstructor);
}
}

View File

@@ -7,7 +7,7 @@ import {
Router,
UrlSegmentGroup
} from '@angular/router';
import { map, switchMap, tap } from 'rxjs/operators';
import { distinctUntilChanged, filter, first, map, switchMap, take, tap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import {
FacetConfigSuccessResponse,
@@ -23,12 +23,12 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import {
configureRequest,
configureRequest, filterSuccessfulResponses,
getResponseFromEntry,
getSucceededRemoteData
} from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
import { NormalizedSearchResult } from '../normalized-search-result.model';
import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model';
@@ -63,6 +63,16 @@ export class SearchService implements OnDestroy {
*/
private facetLinkPathPrefix = 'discover/facets/';
/**
* When true, a new search request is always dispatched
*/
private forceBypassCache = false;
/**
* The ResponseParsingService constructor name
*/
private parser: GenericConstructor<ResponseParsingService> = SearchResponseParsingService;
/**
* Subscription to unsubscribe from
*/
@@ -78,6 +88,19 @@ export class SearchService implements OnDestroy {
) {
}
/**
* Method to set service options
* @param {GenericConstructor<ResponseParsingService>} parser The configuration necessary to perform this search
* @param {boolean} forceBypassCache When true, a new search request is always dispatched
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
*/
setServiceOptions(parser: GenericConstructor<ResponseParsingService>, forceBypassCache: boolean) {
if (parser) {
this.parser = parser;
}
this.forceBypassCache = forceBypassCache;
}
/**
* Method to retrieve a paginated list of search results from the server
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
@@ -90,13 +113,15 @@ export class SearchService implements OnDestroy {
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url);
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
return this.parser;
};
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SearchResponseParsingService;
}
getResponseParser: getResponseParserFn
});
}),
configureRequest(this.requestService)
configureRequest(this.requestService, this.forceBypassCache),
);
const requestEntryObs = requestObs.pipe(
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
@@ -111,8 +136,11 @@ export class SearchService implements OnDestroy {
// turn dspace href from search results to effective list of DSpaceObjects
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
// filter((sqr: SearchQueryResponse) => isNotUndefined(sqr)),
map((sqr: SearchQueryResponse) => {
return sqr.objects.map((nsr: NormalizedSearchResult) => {
return sqr.objects
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.dspaceObject))
.map((nsr: NormalizedSearchResult) => {
return this.rdb.buildSingle(nsr.dspaceObject);
})
}),
@@ -126,7 +154,7 @@ export class SearchService implements OnDestroy {
let co = DSpaceObject;
if (dsos.payload[index]) {
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
co = getSearchResultFor(constructor);
co = getSearchResultFor(constructor, searchOptions.configuration);
return Object.assign(new co(), object, {
dspaceObject: dsos.payload[index]
});
@@ -134,6 +162,7 @@ export class SearchService implements OnDestroy {
return undefined;
}
});
// .filter((object) => isNotUndefined(object));
})
);
@@ -156,7 +185,7 @@ export class SearchService implements OnDestroy {
* @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
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
*/
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
getConfig(scope?: string, configuration?: string): Observable<RemoteData<SearchFilterConfig[]>> {
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
map((url: string) => {
const args: string[] = [];
@@ -165,6 +194,10 @@ export class SearchService implements OnDestroy {
args.push(`scope=${scope}`);
}
if (isNotEmpty(configuration)) {
args.push(`configuration=${configuration}`);
}
if (isNotEmpty(args)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
@@ -176,7 +209,7 @@ export class SearchService implements OnDestroy {
}
});
}),
configureRequest(this.requestService)
configureRequest(this.requestService, this.forceBypassCache)
);
const requestEntryObs = requestObs.pipe(
@@ -202,6 +235,7 @@ export class SearchService implements OnDestroy {
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
*/
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<PaginatedList<FacetValue>>> {
console.log('getFacetValuesFor');
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix + filterConfig.name).pipe(
map((url: string) => {
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
@@ -219,7 +253,8 @@ export class SearchService implements OnDestroy {
}
});
}),
configureRequest(this.requestService)
configureRequest(this.requestService, this.forceBypassCache),
first()
);
const requestEntryObs = requestObs.pipe(

View File

@@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Observable } from 'rxjs';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
@Component({
selector: 'ds-search-settings',
@@ -30,7 +31,7 @@ export class SearchSettingsComponent implements OnInit {
constructor(private service: SearchService,
private route: ActivatedRoute,
private router: Router,
private searchConfigurationService: SearchConfigurationService) {
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
}
/**

View File

@@ -10,6 +10,7 @@
<div id="search-sidebar-content">
<ds-view-mode-switch class="d-none d-md-block"></ds-view-mode-switch>
<div class="sidebar-content">
<ds-search-switch-configuration *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
<ds-search-filters></ds-search-filters>
<ds-search-settings></ds-search-settings>
</div>

View File

@@ -8,6 +8,9 @@
ds-view-mode-switch {
margin-bottom: $spacer;
}
ds-search-switch-configuration {
margin-bottom: 2*$spacer !important;
}
.sidebar-content > *:not(:last-child) {
margin-bottom: 4*$spacer;
display: block;

View File

@@ -1,5 +1,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
@@ -17,6 +19,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
*/
export class SearchSidebarComponent {
/**
* The list of available configuration options
*/
@Input() configurationList: SearchConfigurationOption[];
/**
* The total amount of results
*/

View File

@@ -0,0 +1,4 @@
export interface SearchConfigurationOption {
value: string;
label: string;
}

View File

@@ -0,0 +1,13 @@
<div *ngIf="configurationList?.length > 1" class="search-switch-configuration">
<h5>{{ 'search.switch-configuration.title' | translate}}</h5>
<select class="form-control"
[compareWith]="compare"
[(ngModel)]="selectedOption"
(change)="onSelect($event)">
<option *ngFor="let option of configurationList;" [ngValue]="option.value">
{{option.label | translate}}
</option>
</select>
</div>

View File

@@ -0,0 +1,103 @@
// import { SearchService } from '../../search-service/search.service';
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// import { SearchSettingsComponent } from '../../search-settings/search-settings.component';
// import { Observable } from 'rxjs/Observable';
// import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
// import { SortOptions } from '../../../core/cache/models/sort-options.model';
// import { TranslateModule } from '@ngx-translate/core';
// import { RouterTestingModule } from '@angular/router/testing';
// import { ActivatedRoute } from '@angular/router';
// import { SearchSidebarService } from '../../search-sidebar/search-sidebar.service';
// import { NO_ERRORS_SCHEMA } from '@angular/core';
// import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
// import { By } from '@angular/platform-browser';
//
// describe('SearchSettingsComponent', () => {
//
// let comp: SearchSettingsComponent;
// let fixture: ComponentFixture<SearchSettingsComponent>;
// let searchServiceObject: SearchService;
//
// const pagination: PaginationComponentOptions = new PaginationComponentOptions();
// pagination.id = 'search-results-pagination';
// pagination.currentPage = 1;
// pagination.pageSize = 10;
// const sort: SortOptions = new SortOptions();
// const mockResults = [ 'test', 'data' ];
// const searchServiceStub = {
// searchOptions: { pagination: pagination, sort: sort },
// search: () => mockResults
// };
// const queryParam = 'test query';
// const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
// const activatedRouteStub = {
// queryParams: Observable.of({
// query: queryParam,
// scope: scopeParam
// })
// };
//
// const sidebarService = {
// isCollapsed: Observable.of(true),
// collapse: () => this.isCollapsed = Observable.of(true),
// expand: () => this.isCollapsed = Observable.of(false)
// }
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ],
// declarations: [ SearchSettingsComponent, EnumKeysPipe ],
// providers: [
// { provide: SearchService, useValue: searchServiceStub },
//
// { provide: ActivatedRoute, useValue: activatedRouteStub },
// {
// provide: SearchSidebarService,
// useValue: sidebarService
// },
// ],
// schemas: [ NO_ERRORS_SCHEMA ]
// }).compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(SearchSettingsComponent);
// comp = fixture.componentInstance;
//
// // SearchPageComponent test instance
// fixture.detectChanges();
// searchServiceObject = (comp as any).service;
// spyOn(comp, 'reloadRPP');
// spyOn(comp, 'reloadOrder');
// spyOn(searchServiceObject, 'search').and.callThrough();
//
// });
//
// it('it should show the order settings with the respective selectable options', () => {
// const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
// expect(orderSetting).toBeDefined();
// const childElements = orderSetting.query(By.css('.form-control')).children;
// expect(childElements.length).toEqual(2);
//
// });
//
// it('it should show the size settings with the respective selectable options', () => {
// const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
// expect(pageSizeSetting).toBeDefined();
// const childElements = pageSizeSetting.query(By.css('.form-control')).children;
// expect(childElements.length).toEqual(7);
// });
//
// it('should have the proper order value selected by default', () => {
// const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
// const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'))
// expect(childElementToBeSelected).toBeDefined();
// });
//
// it('should have the proper rpp value selected by default', () => {
// const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
// const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'))
// expect(childElementToBeSelected).toBeDefined();
// });
//
// });

View File

@@ -0,0 +1,54 @@
import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { NavigationExtras, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { hasValue } from '../../shared/empty.util';
import { MYDSPACE_ROUTE, SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type';
import { SearchConfigurationOption } from './search-configuration-option.model';
@Component({
selector: 'ds-search-switch-configuration',
styleUrls: ['./search-switch-configuration.component.scss'],
templateUrl: './search-switch-configuration.component.html',
})
export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit {
/**
* The list of available configuration options
*/
@Input() configurationList: SearchConfigurationOption[] = [];
public selectedOption: string;
private sub: Subscription;
constructor(private router: Router,
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
}
ngOnInit() {
this.searchConfigService.getCurrentConfiguration('default')
.subscribe((currentConfiguration) => this.selectedOption = currentConfiguration);
}
onSelect(event: Event) {
const navigationExtras: NavigationExtras = {
queryParams: {configuration: this.selectedOption},
};
this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
}
compare(item1: MyDSpaceConfigurationValueType, item2: MyDSpaceConfigurationValueType) {
return item1 === item2;
}
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -16,6 +16,7 @@ export function getItemModulePath() {
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },

View File

@@ -2,7 +2,8 @@ import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { merge as observableMerge, Observable, of as observableOf, race as observableRace } from 'rxjs';
import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { remove } from 'lodash';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
@@ -123,7 +124,10 @@ export class RequestService {
// TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
const isGetRequest = request.method === RestRequestMethod.GET;
if (!isGetRequest || !this.isCachedOrPending(request) || (forceBypassCache && !this.isPending(request))) {
if (forceBypassCache) {
this.clearRequestsOnTheirWayToTheStore(request);
}
if (!isGetRequest || (forceBypassCache && !this.isPending(request)) || !this.isCachedOrPending(request)) {
this.dispatchRequest(request);
if (isGetRequest) {
this.trackRequestsOnTheirWayToTheStore(request);
@@ -248,6 +252,19 @@ export class RequestService {
});
}
/**
* This method will store the href of every GET request that gets configured in a local variable, and
* remove it as soon as it can be found in the store.
*/
private clearRequestsOnTheirWayToTheStore(request: GetRequest) {
this.store.pipe(select(this.entryFromUUIDSelector(request.uuid)),
find((re: RequestEntry) => hasValue(re)))
.subscribe((re: RequestEntry) => {
if (!re.responsePending) {
remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href);
}
});
}
/**
* Dispatch commit action to send all changes (for a certain method) to the server (buffer)
* @param {RestRequestMethod} method RestRequestMethod for which the changes should be committed

View File

@@ -37,9 +37,5 @@ export class UserMenuComponent implements OnInit {
// set user
this.user$ = this.store.pipe(select(getAuthenticatedUser));
this.user$.subscribe((user) => {
console.log(user, user.name);
})
}
}

View File

@@ -12,11 +12,19 @@ export class PaginationComponentOptions extends NgbPaginationConfig {
*/
currentPage = 1;
/**
* Maximum number of pages to display.
*/
maxSize = 10;
/**
* A number array that represents options for a context pagination limit.
*/
pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
/**
* Number of items per page.
*/
pageSize: number;
}

View File

@@ -0,0 +1,114 @@
import {
ChangeDetectorRef,
Directive,
Input,
OnChanges,
OnDestroy,
SimpleChanges,
TemplateRef,
ViewContainerRef
} from '@angular/core';
import { combineLatest, Observable, Subscription } from 'rxjs';
import { filter, first, map } from 'rxjs/operators';
import { hasValue } from '../empty.util';
import { RoleService } from '../../core/roles/role.service';
import { RoleType } from '../../core/roles/role-types';
@Directive({
selector: '[dsShowOnlyForRole],[dsShowExceptForRole]'
})
/**
* Structural Directive for showing or hiding a template based on current user role
*/
export class RoleDirective implements OnChanges, OnDestroy {
/**
* The role or list of roles that can show template
*/
@Input() dsShowOnlyForRole: RoleType | RoleType[];
/**
* The role or list of roles that cannot show template
*/
@Input() dsShowExceptForRole: RoleType | RoleType[];
private subs: Subscription[] = [];
constructor(
private roleService: RoleService,
private viewContainer: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
private templateRef: TemplateRef<any>
) {
}
ngOnChanges(changes: SimpleChanges): void {
const onlyChanges = changes.dsShowOnlyForRole;
const exceptChanges = changes.dsShowExceptForRole;
this.hasRoles(this.dsShowOnlyForRole);
if (changes.dsShowOnlyForRole) {
this.validateOnly()
} else if (changes.dsShowExceptForRole) {
this.validateExcept()
}
}
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
/**
* Show template in view container
*/
private showTemplateBlockInView(): void {
this.viewContainer.clear();
if (!this.templateRef) {
return;
}
this.viewContainer.createEmbeddedView(this.templateRef);
this.changeDetector.markForCheck();
}
/**
* Validate the list of roles that can show template
*/
private validateOnly(): void {
this.subs.push(this.hasRoles(this.dsShowOnlyForRole).pipe(filter((hasRole) => hasRole))
.subscribe((hasRole) => {
this.showTemplateBlockInView();
}));
}
/**
* Validate the list of roles that cannot show template
*/
private validateExcept(): void {
this.subs.push(this.hasRoles(this.dsShowExceptForRole).pipe(filter((hasRole) => !hasRole))
.subscribe((hasRole) => {
this.showTemplateBlockInView();
}));
}
/**
* Check if current user role is included in the specified role list
*
* @param roles
* The role or the list of roles
* @returns {Observable<boolean>}
* observable of true if current user role is included in the specified role list, observable of false otherwise
*/
private hasRoles(roles: RoleType | RoleType[]): Observable<boolean> {
const toValidate: RoleType[] = (Array.isArray(roles)) ? roles : [roles];
const checks: Array<Observable<boolean>> = toValidate.map((role) => this.roleService.checkRole(role));
return combineLatest(checks).pipe(
map((permissions: boolean[]) => permissions.includes(true)),
first()
)
}
}

View File

@@ -3,6 +3,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Router } from '@angular/router';
import { hasValue, isNotEmpty } from '../empty.util';
import { QueryParamsHandling } from '@angular/router/src/config';
import { MYDSPACE_ROUTE } from '../../+my-dspace-page/my-dspace-page.component';
/**
* This component renders a simple item page.
@@ -64,7 +65,7 @@ export class SearchFormComponent {
updateSearch(data: any) {
const newUrl = hasValue(this.currentUrl) ? this.currentUrl : '/search';
let handling: QueryParamsHandling = '' ;
if (this.currentUrl === '/search') {
if (this.currentUrl === '/search' || this.currentUrl === MYDSPACE_ROUTE) {
handling = 'merge';
}
this.router.navigate([newUrl], {

View File

@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Params, Router, } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } from '@angular/router';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { isEqual } from 'lodash';
import { AppState } from '../../app.reducer';
import { AddUrlToHistoryAction } from '../history/history.actions';
@@ -16,35 +17,35 @@ export class RouteService {
}
getQueryParameterValues(paramName: string): Observable<string[]> {
return this.route.queryParamMap.pipe(
return this.getQueryParamMap().pipe(
map((params) => [...params.getAll(paramName)]),
distinctUntilChanged()
);
}
getQueryParameterValue(paramName: string): Observable<string> {
return this.route.queryParamMap.pipe(
return this.getQueryParamMap().pipe(
map((params) => params.get(paramName)),
distinctUntilChanged()
);
}
hasQueryParam(paramName: string): Observable<boolean> {
return this.route.queryParamMap.pipe(
return this.getQueryParamMap().pipe(
map((params) => params.has(paramName)),
distinctUntilChanged()
);
}
hasQueryParamWithValue(paramName: string, paramValue: string): Observable<boolean> {
return this.route.queryParamMap.pipe(
return this.getQueryParamMap().pipe(
map((params) => params.getAll(paramName).indexOf(paramValue) > -1),
distinctUntilChanged()
);
}
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
return this.route.queryParamMap.pipe(
return this.getQueryParamMap().pipe(
map((qparams) => {
const params = {};
qparams.keys
@@ -57,6 +58,19 @@ export class RouteService {
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)));
}
public getQueryParamMap(): Observable<any> {
return this.route.queryParamMap.pipe(
map((paramMap) => {
const snapshot: RouterStateSnapshot = this.router.routerState.snapshot;
// Due to an Angular bug, sometimes change of QueryParam is not detected so double checks with route snapshot
if (!isEqual(paramMap, snapshot.root.queryParamMap)) {
return snapshot.root.queryParamMap;
} else {
return paramMap;
}
}))
}
public saveRouting(): void {
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))

View File

@@ -77,6 +77,22 @@ import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { AlertsComponent } from './alerts/alerts.component';
import { MyDSpaceResultListElementComponent } from './object-list/my-dspace-result-list-element/my-dspace-result-list-element.component';
import { MessageBoardComponent } from './message-board/message-board.component';
import { MessageComponent } from './message-board/message/message.component';
import { MyDSpaceResultDetailElementComponent } from './object-detail/my-dspace-result-detail-element/my-dspace-result-detail-element.component';
import { ClaimedTaskActionsComponent } from './mydspace-actions/claimed-task/claimed-task-actions.component';
import { PoolTaskActionsComponent } from './mydspace-actions/pool-task/pool-task-actions.component';
import { ObjectDetailComponent } from './object-detail/object-detail.component';
import { WrapperDetailElementComponent } from './object-detail/wrapper-detail-element/wrapper-detail-element.component';
import { ItemDetailPreviewComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component';
import { MyDSpaceItemStatusComponent } from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component';
import { WorkspaceitemActionsComponent } from './mydspace-actions/workspaceitem/workspaceitem-actions.component';
import { WorkflowitemActionsComponent } from './mydspace-actions/workflowitem/workflowitem-actions.component';
import { ItemSubmitterComponent } from './object-collection/shared/mydspace-item-submitter/item-submitter.component';
import { ItemActionsComponent } from './mydspace-actions/item/item-actions.component';
import { ClaimedTaskActionsApproveComponent } from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component';
import { ClaimedTaskActionsRejectComponent } from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component';
import { ObjNgFor } from './utils/object-ngfor.pipe';
import { BrowseByComponent } from './browse-by/browse-by.component';
import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component';
@@ -95,6 +111,20 @@ import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-co
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
import { ItemListPreviewComponent } from './object-list/item-list-preview/item-list-preview.component';
import { ItemPageAuthorFieldComponent } from '../+item-page/simple/field-components/specific-field/author/item-page-author-field.component';
import { ItemPageDateFieldComponent } from '../+item-page/simple/field-components/specific-field/date/item-page-date-field.component';
import { ItemPageAbstractFieldComponent } from '../+item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component';
import { ItemPageUriFieldComponent } from '../+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component';
import { ItemPageTitleFieldComponent } from '../+item-page/simple/field-components/specific-field/title/item-page-title-field.component';
import { ItemPageSpecificFieldComponent } from '../+item-page/simple/field-components/specific-field/item-page-specific-field.component';
import { FileSectionComponent } from '../+item-page/simple/field-components/file-section/file-section.component';
import { MetadataFieldWrapperComponent } from '../+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { CollectionsComponent } from '../+item-page/field-components/collections/collections.component';
import { MetadataValuesComponent } from '../+item-page/field-components/metadata-values/metadata-values.component';
import { MetadataUriValuesComponent } from '../+item-page/field-components/metadata-uri-values/metadata-uri-values.component';
import { RoleDirective } from './roles/role.directive';
import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -136,6 +166,7 @@ const COMPONENTS = [
// put shared components here
AlertsComponent,
AuthNavMenuComponent,
UserMenuComponent,
ChipsComponent,
ComcolPageContentComponent,
ComcolPageHeaderComponent,
@@ -163,11 +194,15 @@ const COMPONENTS = [
LoadingComponent,
LogInComponent,
LogOutComponent,
MessageBoardComponent,
MessageComponent,
NumberPickerComponent,
ObjectListComponent,
ObjectDetailComponent,
ObjectGridComponent,
AbstractListableElementComponent,
WrapperListElementComponent,
ObjectGridComponent,
WrapperDetailElementComponent,
WrapperGridElementComponent,
ObjectCollectionComponent,
PaginationComponent,
@@ -176,6 +211,17 @@ const COMPONENTS = [
GridThumbnailComponent,
UploaderComponent,
WrapperListElementComponent,
ItemListPreviewComponent,
MyDSpaceItemStatusComponent,
ItemSubmitterComponent,
ItemDetailPreviewComponent,
ClaimedTaskActionsComponent,
ClaimedTaskActionsApproveComponent,
ClaimedTaskActionsRejectComponent,
ItemActionsComponent,
PoolTaskActionsComponent,
WorkflowitemActionsComponent,
WorkspaceitemActionsComponent,
ViewModeSwitchComponent,
TruncatableComponent,
TruncatablePartComponent,
@@ -188,12 +234,15 @@ const ENTRY_COMPONENTS = [
ItemListElementComponent,
CollectionListElementComponent,
CommunityListElementComponent,
MyDSpaceResultListElementComponent,
SearchResultListElementComponent,
ItemGridElementComponent,
CollectionGridElementComponent,
CommunityGridElementComponent,
SearchResultGridElementComponent,
BrowseEntryListElementComponent,
MyDSpaceResultDetailElementComponent,
SearchResultGridElementComponent,
DsDynamicListComponent,
DsDynamicLookupComponent,
DsDynamicScrollableDropdownComponent,
@@ -206,6 +255,20 @@ const ENTRY_COMPONENTS = [
DsDatePickerInlineComponent
];
const SHARED_ITEM_PAGE_COMPONENTS = [
CollectionsComponent,
FileSectionComponent,
ItemPageAuthorFieldComponent,
ItemPageDateFieldComponent,
ItemPageAbstractFieldComponent,
ItemPageUriFieldComponent,
ItemPageTitleFieldComponent,
ItemPageSpecificFieldComponent,
MetadataFieldWrapperComponent,
MetadataValuesComponent,
MetadataUriValuesComponent
];
const PROVIDERS = [
TruncatableService,
MockAdminGuard,
@@ -220,7 +283,8 @@ const DIRECTIVES = [
DragClickDirective,
DebounceDirective,
ClickOutsideDirective,
AuthorityConfidenceStateDirective
AuthorityConfidenceStateDirective,
RoleDirective
];
@NgModule({
@@ -232,6 +296,7 @@ const DIRECTIVES = [
...COMPONENTS,
...DIRECTIVES,
...ENTRY_COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS
],
providers: [
...PROVIDERS
@@ -240,6 +305,7 @@ const DIRECTIVES = [
...MODULES,
...PIPES,
...COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS,
...DIRECTIVES
],
entryComponents: [

View File

@@ -1,3 +1,3 @@
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse">
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse()">
<ng-content></ng-content>
</div>