mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-62372_Final-master-merge
Conflicts: config/environment.default.js resources/i18n/en.json src/app/+search-page/paginated-search-options.model.ts src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts src/app/+search-page/search-options.model.ts src/app/+search-page/search-page.component.spec.ts src/app/+search-page/search-page.component.ts src/app/+search-page/search-page.module.ts src/app/+search-page/search-service/search-configuration.service.ts src/app/core/cache/response.models.ts src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts src/app/shared/services/route.service.ts
This commit is contained in:
@@ -392,6 +392,7 @@
|
|||||||
},
|
},
|
||||||
"login": "Log In",
|
"login": "Log In",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
|
"mydspace": "MyDSpace",
|
||||||
"language": "Language switch",
|
"language": "Language switch",
|
||||||
"search": "Search"
|
"search": "Search"
|
||||||
},
|
},
|
||||||
@@ -428,6 +429,57 @@
|
|||||||
"help": "Select a community to browse its collections."
|
"help": "Select a community to browse its collections."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mydspace": {
|
||||||
|
"title": "MyDSpace",
|
||||||
|
"description": "",
|
||||||
|
"new-submission": "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",
|
||||||
|
"no-files": "No Files",
|
||||||
|
"no-uri": "No Uri",
|
||||||
|
"no-collections": "No Collections"
|
||||||
|
},
|
||||||
|
"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": "Workflow",
|
||||||
|
"validation": "Validation",
|
||||||
|
"waiting-for-controller": "Waiting for controller",
|
||||||
|
"workspace": "Workspace",
|
||||||
|
"archived": "Archived"
|
||||||
|
},
|
||||||
|
"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": {
|
"search": {
|
||||||
"journal": {
|
"journal": {
|
||||||
"title": "DSpace Angular :: Journal Search",
|
"title": "DSpace Angular :: Journal Search",
|
||||||
@@ -451,7 +503,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"form": {
|
"form": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_dspace": "Search DSpace"
|
"search_dspace": "Search DSpace",
|
||||||
|
"search_mydspace": "Search MyDSpace"
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"head": "Search Results",
|
"head": "Search Results",
|
||||||
@@ -471,9 +524,13 @@
|
|||||||
"rpp": "Results per page"
|
"rpp": "Results per page"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"switch-configuration": {
|
||||||
|
"title":"Show"
|
||||||
|
},
|
||||||
"view-switch": {
|
"view-switch": {
|
||||||
"show-list": "Show as list",
|
"show-list": "Show as list",
|
||||||
"show-grid": "Show as grid"
|
"show-grid": "Show as grid",
|
||||||
|
"show-detail": "Show detail"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"head": "Filters",
|
"head": "Filters",
|
||||||
@@ -484,7 +541,11 @@
|
|||||||
"f.dateIssued.max": "End date",
|
"f.dateIssued.max": "End date",
|
||||||
"f.subject": "Subject",
|
"f.subject": "Subject",
|
||||||
"f.has_content_in_original_bundle": "Has files",
|
"f.has_content_in_original_bundle": "Has files",
|
||||||
"f.entityType": "Item Type"
|
"f.entityType": "Item Type",
|
||||||
|
"f.namedresourcetype": "Status",
|
||||||
|
"f.dateSubmitted": "Date submitted",
|
||||||
|
"f.itemtype": "Type",
|
||||||
|
"f.submitter": "Submitter"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"show-more": "Show more",
|
"show-more": "Show more",
|
||||||
@@ -516,6 +577,26 @@
|
|||||||
"entityType": {
|
"entityType": {
|
||||||
"placeholder": "Item Type",
|
"placeholder": "Item Type",
|
||||||
"head": "Item Type"
|
"head": "Item Type"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -736,6 +817,7 @@
|
|||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading...",
|
"objects": "Loading...",
|
||||||
"search-results": "Loading search results...",
|
"search-results": "Loading search results...",
|
||||||
|
"mydspace-results": "Loading items...",
|
||||||
"browse-by": "Loading items...",
|
"browse-by": "Loading items...",
|
||||||
"browse-by-page": "Loading page..."
|
"browse-by-page": "Loading page..."
|
||||||
},
|
},
|
||||||
@@ -927,6 +1009,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": {
|
"uploader": {
|
||||||
|
@@ -28,6 +28,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
getActiveMetadataField: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
|
cancelEditMetadataField: () => {},
|
||||||
cancelEditMetadataSchema: () => {},
|
cancelEditMetadataSchema: () => {},
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
@@ -62,6 +63,11 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
registryService = s;
|
registryService = s;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component = null;
|
||||||
|
registryService = null
|
||||||
|
})
|
||||||
|
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
const element = 'fakeElement';
|
const element = 'fakeElement';
|
||||||
const qualifier = 'fakeQualifier';
|
const qualifier = 'fakeQualifier';
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
@@ -6,9 +6,7 @@ import { GenericItemPageFieldComponent } from './simple/field-components/specifi
|
|||||||
|
|
||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
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 { 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 { 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 { 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 { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||||
@@ -44,9 +42,7 @@ import { RelatedEntitiesSearchComponent } from './simple/related-entities/relate
|
|||||||
declarations: [
|
declarations: [
|
||||||
ItemPageComponent,
|
ItemPageComponent,
|
||||||
FullItemPageComponent,
|
FullItemPageComponent,
|
||||||
MetadataValuesComponent,
|
|
||||||
MetadataUriValuesComponent,
|
MetadataUriValuesComponent,
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
ItemPageAuthorFieldComponent,
|
ItemPageAuthorFieldComponent,
|
||||||
ItemPageDateFieldComponent,
|
ItemPageDateFieldComponent,
|
||||||
ItemPageAbstractFieldComponent,
|
ItemPageAbstractFieldComponent,
|
||||||
|
@@ -61,6 +61,6 @@ export class ItemPageComponent implements OnInit {
|
|||||||
this.thumbnail$ = this.itemRD$.pipe(
|
this.thumbnail$ = this.itemRD$.pipe(
|
||||||
map((rd: RemoteData<Item>) => rd.payload),
|
map((rd: RemoteData<Item>) => rd.payload),
|
||||||
filter((item: Item) => hasValue(item)),
|
filter((item: Item) => hasValue(item)),
|
||||||
mergeMap((item: Item) => item.getThumbnail()),);
|
mergeMap((item: Item) => item.getThumbnail()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
export enum MyDSpaceConfigurationValueType {
|
||||||
|
Workspace = 'workspace',
|
||||||
|
Workflow = 'workflow'
|
||||||
|
}
|
257
src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts
Normal file
257
src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { SearchFilter } from '../+search-page/search-filter.model';
|
||||||
|
import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
|
||||||
|
import { MockRoleService } from '../shared/mocks/mock-role-service';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||||
|
|
||||||
|
describe('MyDSpaceConfigurationService', () => {
|
||||||
|
let service: MyDSpaceConfigurationService;
|
||||||
|
const value1 = 'random value';
|
||||||
|
const prefixFilter = {
|
||||||
|
'f.namedresourcetype': ['another value'],
|
||||||
|
'f.dateSubmitted.min': ['2013'],
|
||||||
|
'f.dateSubmitted.max': ['2018']
|
||||||
|
};
|
||||||
|
const defaults = new PaginatedSearchOptions({
|
||||||
|
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
|
||||||
|
sort: new SortOptions('score', SortDirection.DESC),
|
||||||
|
query: '',
|
||||||
|
scope: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const backendFilters = [new SearchFilter('f.namedresourcetype', ['another value']), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'])];
|
||||||
|
|
||||||
|
const spy = jasmine.createSpyObj('RouteService', {
|
||||||
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
getQueryParamsWithPrefix: observableOf(prefixFilter)
|
||||||
|
});
|
||||||
|
|
||||||
|
const activatedRoute: any = new ActivatedRouteStub();
|
||||||
|
|
||||||
|
const roleService: any = new MockRoleService();
|
||||||
|
|
||||||
|
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
|
||||||
|
getQueryByFilterName: observableOf(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new MyDSpaceConfigurationService(roleService, fixedFilterService, spy, activatedRoute);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the scope is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentScope('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'scope\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('scope');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentConfiguration is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentConfiguration('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'configuration\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('configuration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentQuery is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentQuery('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'query\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('query');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentDSOType is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentDSOType();
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'dsoType\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('dsoType');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentFrontendFilters is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentFrontendFilters();
|
||||||
|
});
|
||||||
|
it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentFilters is called', () => {
|
||||||
|
let parsedValues$;
|
||||||
|
beforeEach(() => {
|
||||||
|
parsedValues$ = service.getCurrentFilters();
|
||||||
|
});
|
||||||
|
it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.');
|
||||||
|
parsedValues$.subscribe((values) => {
|
||||||
|
expect(values).toEqual(backendFilters);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentSort is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentSort({} as any);
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentPagination is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToSearchOptions or subscribeToPaginatedSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'getCurrentPagination').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentSort').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentScope').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentConfiguration').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentQuery').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentDSOType').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentFilters').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).subscribeToSearchOptions(defaults)
|
||||||
|
});
|
||||||
|
it('should call all getters it needs, but not call any others', () => {
|
||||||
|
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentSort).not.toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||||
|
});
|
||||||
|
it('should call all getters it needs', () => {
|
||||||
|
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentSort).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getAvailableConfigurationTypes is called', () => {
|
||||||
|
|
||||||
|
it('should return properly list when user is submitter', () => {
|
||||||
|
roleService.setSubmitter(true);
|
||||||
|
roleService.setController(false);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is controller', () => {
|
||||||
|
roleService.setSubmitter(false);
|
||||||
|
roleService.setController(true);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is admin', () => {
|
||||||
|
roleService.setSubmitter(false);
|
||||||
|
roleService.setController(false);
|
||||||
|
roleService.setAdmin(true);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is submitter and controller', () => {
|
||||||
|
roleService.setSubmitter(true);
|
||||||
|
roleService.setController(true);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getAvailableConfigurationOptions is called', () => {
|
||||||
|
|
||||||
|
it('should return properly options list', () => {
|
||||||
|
spyOn(service, 'getAvailableConfigurationTypes').and.returnValue(hot('a', {
|
||||||
|
a: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationOptions();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
label: `mydspace.show.${MyDSpaceConfigurationValueType.Workspace}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workflow,
|
||||||
|
label: `mydspace.show.${MyDSpaceConfigurationValueType.Workflow}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
120
src/app/+my-dspace-page/my-dspace-configuration.service.ts
Normal file
120
src/app/+my-dspace-page/my-dspace-configuration.service.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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';
|
||||||
|
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that performs all actions that have to do with the current mydspace configuration
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MyDSpaceConfigurationService extends SearchConfigurationService {
|
||||||
|
/**
|
||||||
|
* Default pagination settings
|
||||||
|
*/
|
||||||
|
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'mydspace-page',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default sort settings
|
||||||
|
*/
|
||||||
|
protected defaultSort = new SortOptions('dc.date.issued', SortDirection.DESC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration parameter setting
|
||||||
|
*/
|
||||||
|
protected defaultConfiguration = 'workspace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default scope setting
|
||||||
|
*/
|
||||||
|
protected defaultScope = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default query setting
|
||||||
|
*/
|
||||||
|
protected defaultQuery = '';
|
||||||
|
|
||||||
|
private isAdmin$: Observable<boolean>;
|
||||||
|
private isController$: Observable<boolean>;
|
||||||
|
private isSubmitter$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize class
|
||||||
|
*
|
||||||
|
* @param {roleService} roleService
|
||||||
|
* @param {SearchFixedFilterService} fixedFilterService
|
||||||
|
* @param {RouteService} routeService
|
||||||
|
* @param {ActivatedRoute} route
|
||||||
|
*/
|
||||||
|
constructor(protected roleService: RoleService,
|
||||||
|
protected fixedFilterService: SearchFixedFilterService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected route: ActivatedRoute) {
|
||||||
|
|
||||||
|
super(routeService, fixedFilterService, route);
|
||||||
|
|
||||||
|
// override parent class initialization
|
||||||
|
this._defaults = null;
|
||||||
|
this.initDefaults();
|
||||||
|
|
||||||
|
this.isSubmitter$ = this.roleService.isSubmitter();
|
||||||
|
this.isController$ = this.roleService.isController();
|
||||||
|
this.isAdmin$ = this.roleService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of available configuration depend on the user role
|
||||||
|
*
|
||||||
|
* @return {Observable<MyDSpaceConfigurationValueType[]>}
|
||||||
|
* Emits the available configuration list
|
||||||
|
*/
|
||||||
|
public getAvailableConfigurationTypes(): Observable<MyDSpaceConfigurationValueType[]> {
|
||||||
|
return combineLatest(this.isSubmitter$, this.isController$, this.isAdmin$).pipe(
|
||||||
|
first(),
|
||||||
|
map(([isSubmitter, isController, isAdmin]: [boolean, boolean, boolean]) => {
|
||||||
|
const availableConf: MyDSpaceConfigurationValueType[] = [];
|
||||||
|
if (isSubmitter) {
|
||||||
|
availableConf.push(MyDSpaceConfigurationValueType.Workspace);
|
||||||
|
}
|
||||||
|
if (isController || isAdmin) {
|
||||||
|
availableConf.push(MyDSpaceConfigurationValueType.Workflow);
|
||||||
|
}
|
||||||
|
return availableConf;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the select options for the available configuration list
|
||||||
|
*
|
||||||
|
* @return {Observable<SearchConfigurationOption[]>}
|
||||||
|
* Emits the select options list
|
||||||
|
*/
|
||||||
|
public getAvailableConfigurationOptions(): Observable<SearchConfigurationOption[]> {
|
||||||
|
return this.getAvailableConfigurationTypes().pipe(
|
||||||
|
first(),
|
||||||
|
map((availableConfigurationTypes: MyDSpaceConfigurationValueType[]) => {
|
||||||
|
const configurationOptions: SearchConfigurationOption[] = [];
|
||||||
|
availableConfigurationTypes.forEach((type) => {
|
||||||
|
const value = type;
|
||||||
|
const label = `mydspace.show.${value}`;
|
||||||
|
configurationOptions.push({ value, label });
|
||||||
|
});
|
||||||
|
return configurationOptions;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="parent mb-3">
|
||||||
|
<div class="upload">
|
||||||
|
<ds-uploader *ngIf="uploadFilesOptions.url !== ''"
|
||||||
|
[uploadFilesOptions]="uploadFilesOptions"
|
||||||
|
(onCompleteItem)="onCompleteItem($event)"
|
||||||
|
(onUploadError)="onUploadError($event)"></ds-uploader>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="add">
|
||||||
|
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/submit']" role="button">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,11 @@
|
|||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
flex: initial;
|
||||||
|
}
|
@@ -0,0 +1,101 @@
|
|||||||
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { createTestComponent } from '../../shared/testing/utils';
|
||||||
|
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/mock-translate.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { getMockScrollToService } from '../../shared/mocks/mock-scroll-to-service';
|
||||||
|
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||||
|
|
||||||
|
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||||
|
|
||||||
|
const translateService: any = getMockTranslateService();
|
||||||
|
const store: Store<AppState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
pipe: observableOf(true)
|
||||||
|
});
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
MyDSpaceNewSubmissionComponent,
|
||||||
|
TestComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: TranslateService, useValue: translateService },
|
||||||
|
ChangeDetectorRef,
|
||||||
|
MyDSpaceNewSubmissionComponent,
|
||||||
|
UploaderService
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let testComp: TestComponent;
|
||||||
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
const html = `
|
||||||
|
<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testFixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create MyDSpaceNewSubmissionComponent', inject([MyDSpaceNewSubmissionComponent], (app: MyDSpaceNewSubmissionComponent) => {
|
||||||
|
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
|
||||||
|
reload = (event) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SubmissionState } from '../../submission/submission.reducers';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { MyDSpaceResult } from '../my-dspace-result.model';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
|
import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents the whole mydspace page header
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-new-submission',
|
||||||
|
styleUrls: ['./my-dspace-new-submission.component.scss'],
|
||||||
|
templateUrl: './my-dspace-new-submission.component.html'
|
||||||
|
})
|
||||||
|
export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
||||||
|
@Output() uploadEnd = new EventEmitter<Array<MyDSpaceResult<DSpaceObject>>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UploaderOptions object
|
||||||
|
*/
|
||||||
|
public uploadFilesOptions: UploaderOptions = {
|
||||||
|
url: '',
|
||||||
|
authToken: null,
|
||||||
|
disableMultipart: false,
|
||||||
|
itemAlias: null
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from
|
||||||
|
*/
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {AuthService} authService
|
||||||
|
* @param {ChangeDetectorRef} changeDetectorRef
|
||||||
|
* @param {HALEndpointService} halService
|
||||||
|
* @param {NotificationsService} notificationsService
|
||||||
|
* @param {Store<SubmissionState>} store
|
||||||
|
* @param {TranslateService} translate
|
||||||
|
*/
|
||||||
|
constructor(private authService: AuthService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private halService: HALEndpointService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private store: Store<SubmissionState>,
|
||||||
|
private translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize url and Bearer token
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => {
|
||||||
|
this.uploadFilesOptions.url = url;
|
||||||
|
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when file upload is completed to notify upload status
|
||||||
|
*/
|
||||||
|
public onCompleteItem(res) {
|
||||||
|
if (res && res._embedded && res._embedded.workspaceitems && res._embedded.workspaceitems.length > 0) {
|
||||||
|
const workspaceitems = res._embedded.workspaceitems;
|
||||||
|
this.uploadEnd.emit(workspaceitems);
|
||||||
|
|
||||||
|
if (workspaceitems.length === 1) {
|
||||||
|
const options = new NotificationOptions();
|
||||||
|
options.timeOut = 0;
|
||||||
|
const link = '/workspaceitems/' + workspaceitems[0].id + '/edit';
|
||||||
|
this.notificationsService.notificationWithAnchor(
|
||||||
|
NotificationType.Success,
|
||||||
|
options,
|
||||||
|
link,
|
||||||
|
'mydspace.general.text-here',
|
||||||
|
'mydspace.upload.upload-successful',
|
||||||
|
'here');
|
||||||
|
} else if (workspaceitems.length > 1) {
|
||||||
|
this.notificationsService.success(null, this.translate.get('mydspace.upload.upload-multiple-successful', {qty: workspaceitems.length}));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on file upload error
|
||||||
|
*/
|
||||||
|
public onUploadError() {
|
||||||
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/app/+my-dspace-page/my-dspace-page-routing.module.ts
Normal file
25
src/app/+my-dspace-page/my-dspace-page-routing.module.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||||
|
import { MyDSpaceGuard } from './my-dspace.guard';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: MyDSpacePageComponent,
|
||||||
|
data: { title: 'mydspace.title' },
|
||||||
|
canActivate: [
|
||||||
|
MyDSpaceGuard
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module defines the default component to load when navigating to the mydspace page path.
|
||||||
|
*/
|
||||||
|
export class MyDspacePageRoutingModule {
|
||||||
|
}
|
45
src/app/+my-dspace-page/my-dspace-page.component.html
Normal file
45
src/app/+my-dspace-page/my-dspace-page.component.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"
|
||||||
|
(uploadEnd)="reload($event)"></ds-my-dspace-new-submission>
|
||||||
|
<div class="search-page row">
|
||||||
|
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
||||||
|
id="search-sidebar"
|
||||||
|
[configurationList]="(configurationList$ | async)"
|
||||||
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
|
[viewModeList]="viewModeList"></ds-search-sidebar>
|
||||||
|
<div class="col-12 col-md-9">
|
||||||
|
<ds-search-form id="search-form"
|
||||||
|
[query]="(searchOptions$ | async)?.query"
|
||||||
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
|
[currentUrl]="getSearchLink()"
|
||||||
|
[scopes]="(scopeListRD$ | async)">
|
||||||
|
</ds-search-form>
|
||||||
|
<ds-search-labels></ds-search-labels>
|
||||||
|
<div class="row">
|
||||||
|
<div id="search-body"
|
||||||
|
class="row-offcanvas row-offcanvas-left"
|
||||||
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
|
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||||
|
id="search-sidebar-sm"
|
||||||
|
[configurationList]="(configurationList$ | async)"
|
||||||
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
|
(toggleSidebar)="closeSidebar()"
|
||||||
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
|
||||||
|
</ds-search-sidebar>
|
||||||
|
<div id="search-content" class="col-12">
|
||||||
|
<div class="d-block d-md-none search-controls clearfix">
|
||||||
|
<ds-view-mode-switch [viewModeList]="viewModeList"></ds-view-mode-switch>
|
||||||
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
|
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||||
|
| translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ds-my-dspace-results [searchResults]="resultsRD$ | async"
|
||||||
|
[searchConfig]="searchOptions$ | async"></ds-my-dspace-results>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
1
src/app/+my-dspace-page/my-dspace-page.component.scss
Normal file
1
src/app/+my-dspace-page/my-dspace-page.component.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '../+search-page/search-page.component.scss';
|
194
src/app/+my-dspace-page/my-dspace-page.component.spec.ts
Normal file
194
src/app/+my-dspace-page/my-dspace-page.component.spec.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { routeServiceStub } from '../shared/testing/route-service-stub';
|
||||||
|
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||||
|
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
|
||||||
|
import { RoleDirective } from '../shared/roles/role.directive';
|
||||||
|
import { RoleService } from '../core/roles/role.service';
|
||||||
|
import { MockRoleService } from '../shared/mocks/mock-role-service';
|
||||||
|
|
||||||
|
describe('MyDSpacePageComponent', () => {
|
||||||
|
let comp: MyDSpacePageComponent;
|
||||||
|
let fixture: ComponentFixture<MyDSpacePageComponent>;
|
||||||
|
let searchServiceObject: SearchService;
|
||||||
|
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||||
|
const store: Store<MyDSpacePageComponent> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: observableOf(true)
|
||||||
|
});
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'mydspace-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
|
const mockResults = observableOf(new RemoteData(false, false, true, null, ['test', 'data']));
|
||||||
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
|
search: mockResults,
|
||||||
|
getSearchLink: '/mydspace',
|
||||||
|
getScopes: observableOf(['test-scope']),
|
||||||
|
setServiceOptions: {}
|
||||||
|
});
|
||||||
|
const configurationParam = 'default';
|
||||||
|
const queryParam = 'test query';
|
||||||
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
|
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||||
|
configuration: configurationParam,
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam,
|
||||||
|
pagination,
|
||||||
|
sort
|
||||||
|
});
|
||||||
|
const activatedRouteStub = {
|
||||||
|
snapshot: {
|
||||||
|
queryParamMap: new Map([
|
||||||
|
['query', queryParam],
|
||||||
|
['scope', scopeParam]
|
||||||
|
])
|
||||||
|
},
|
||||||
|
queryParams: observableOf({
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const sidebarService = {
|
||||||
|
isCollapsed: observableOf(true),
|
||||||
|
collapse: () => this.isCollapsed = observableOf(true),
|
||||||
|
expand: () => this.isCollapsed = observableOf(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
||||||
|
declarations: [MyDSpacePageComponent, RoleDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{
|
||||||
|
provide: CommunityDataService,
|
||||||
|
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
||||||
|
},
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{
|
||||||
|
provide: Store, useValue: store
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||||
|
{
|
||||||
|
isXs: observableOf(true),
|
||||||
|
isSm: observableOf(false),
|
||||||
|
isXsOrSm: observableOf(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SearchSidebarService,
|
||||||
|
useValue: sidebarService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SearchFilterService,
|
||||||
|
useValue: {}
|
||||||
|
}, {
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useValue: new SearchConfigurationServiceStub()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: RoleService,
|
||||||
|
useValue: new MockRoleService()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(MyDSpacePageComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpacePageComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
searchServiceObject = (comp as any).service;
|
||||||
|
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
searchServiceObject = null;
|
||||||
|
searchConfigurationServiceObject = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the scope and query from the route parameters', () => {
|
||||||
|
|
||||||
|
searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions);
|
||||||
|
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||||
|
b: paginatedSearchOptions
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the open sidebar button is clicked in mobile view', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'openSidebar');
|
||||||
|
const openSidebarButton = fixture.debugElement.query(By.css('.open-sidebar'));
|
||||||
|
openSidebarButton.triggerEventHandler('click', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger the openSidebar function', () => {
|
||||||
|
expect(comp.openSidebar).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||||
|
let menu: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
|
comp.isSidebarCollapsed = () => observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the sidebar', () => {
|
||||||
|
expect(menu.classList).not.toContain('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||||
|
let menu: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
|
comp.isSidebarCollapsed = () => observableOf(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the menu', () => {
|
||||||
|
expect(menu.classList).toContain('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
156
src/app/+my-dspace-page/my-dspace-page.component.ts
Normal file
156
src/app/+my-dspace-page/my-dspace-page.component.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject, InjectionToken, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
|
import { switchMap, tap, } 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';
|
||||||
|
import { pushInOut } from '../shared/animations/push';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { MyDSpaceResult } from './my-dspace-result.model';
|
||||||
|
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
|
||||||
|
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
|
||||||
|
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 { ViewMode } from '../core/shared/view-mode.model';
|
||||||
|
import { MyDSpaceRequest } from '../core/data/request.models';
|
||||||
|
|
||||||
|
export const MYDSPACE_ROUTE = '/mydspace';
|
||||||
|
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents the whole mydspace page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-page',
|
||||||
|
styleUrls: ['./my-dspace-page.component.scss'],
|
||||||
|
templateUrl: './my-dspace-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [pushInOut],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: MyDSpaceConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MyDSpacePageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available configuration options
|
||||||
|
*/
|
||||||
|
configurationList$: Observable<SearchConfigurationOption[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current search results
|
||||||
|
*/
|
||||||
|
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current paginated search options
|
||||||
|
*/
|
||||||
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current relevant scopes
|
||||||
|
*/
|
||||||
|
scopeListRD$: Observable<DSpaceObject[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true if were on a small screen
|
||||||
|
*/
|
||||||
|
isXsOrSm$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from
|
||||||
|
*/
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable for enumeration RoleType
|
||||||
|
*/
|
||||||
|
roleTypeEnum = RoleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of available view mode
|
||||||
|
*/
|
||||||
|
viewModeList = [ViewMode.List, ViewMode.Detail];
|
||||||
|
|
||||||
|
constructor(private service: SearchService,
|
||||||
|
private sidebarService: SearchSidebarService,
|
||||||
|
private windowService: HostWindowService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
|
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize available configuration list
|
||||||
|
*
|
||||||
|
* Listening to changes in the paginated search options
|
||||||
|
* If something changes, update the search results
|
||||||
|
*
|
||||||
|
* Listen to changes in the scope
|
||||||
|
* If something changes, update the list of scopes for the dropdown
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
|
||||||
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
|
|
||||||
|
this.sub = this.searchOptions$.pipe(
|
||||||
|
tap(() => this.resultsRD$.next(null)),
|
||||||
|
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData())))
|
||||||
|
.subscribe((results) => {
|
||||||
|
this.resultsRD$.next(results);
|
||||||
|
});
|
||||||
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
|
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sidebar to a collapsed state
|
||||||
|
*/
|
||||||
|
public closeSidebar(): void {
|
||||||
|
this.sidebarService.collapse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sidebar to an expanded state
|
||||||
|
*/
|
||||||
|
public openSidebar(): void {
|
||||||
|
this.sidebarService.expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the sidebar is collapsed
|
||||||
|
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||||
|
*/
|
||||||
|
public isSidebarCollapsed(): Observable<boolean> {
|
||||||
|
return this.sidebarService.isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} The base path to the search page
|
||||||
|
*/
|
||||||
|
public getSearchLink(): string {
|
||||||
|
return this.service.getSearchLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/app/+my-dspace-page/my-dspace-page.module.ts
Normal file
69
src/app/+my-dspace-page/my-dspace-page.module.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
|
import { MyDspacePageRoutingModule } from './my-dspace-page-routing.module';
|
||||||
|
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||||
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
import { MyDSpaceResultsComponent } from './my-dspace-results/my-dspace-results.component';
|
||||||
|
import { WorkspaceitemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workspaceitem-my-dspace-result/workspaceitem-my-dspace-result-list-element.component';
|
||||||
|
import { ItemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/item-my-dspace-result/item-my-dspace-result-list-element.component';
|
||||||
|
import { WorkflowitemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflowitem-my-dspace-result/workflowitem-my-dspace-result-list-element.component';
|
||||||
|
import { ClaimedMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-my-dspace-result/claimed-my-dspace-result-list-element.component';
|
||||||
|
import { PoolMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/pool-my-dspace-result/pool-my-dspace-result-list-element.component';
|
||||||
|
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission/my-dspace-new-submission.component';
|
||||||
|
import { ItemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-my-dspace-result/item-my-dspace-result-detail-element.component';
|
||||||
|
import { WorkspaceitemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workspaceitem-my-dspace-result/workspaceitem-my-dspace-result-detail-element.component';
|
||||||
|
import { WorkflowitemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workflowitem-my-dspace-result/workflowitem-my-dspace-result-detail-element.component';
|
||||||
|
import { ClaimedMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/claimed-my-dspace-result/claimed-my-dspace-result-detail-element.component';
|
||||||
|
import { PoolMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-my-dspace-result/pool-my-dspace-result-detail-lement.component';
|
||||||
|
import { MyDSpaceGuard } from './my-dspace.guard';
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
MyDspacePageRoutingModule,
|
||||||
|
SearchPageModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
MyDSpacePageComponent,
|
||||||
|
MyDSpaceResultsComponent,
|
||||||
|
ItemMyDSpaceResultListElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultListElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultListElementComponent,
|
||||||
|
ClaimedMyDSpaceResultListElementComponent,
|
||||||
|
PoolMyDSpaceResultListElementComponent,
|
||||||
|
ItemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultDetailElementComponent,
|
||||||
|
ClaimedMyDSpaceResultDetailElementComponent,
|
||||||
|
PoolMyDSpaceResultDetailElementComponent,
|
||||||
|
MyDSpaceNewSubmissionComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MyDSpaceGuard,
|
||||||
|
MyDSpaceConfigurationService
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ItemMyDSpaceResultListElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultListElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultListElementComponent,
|
||||||
|
ClaimedMyDSpaceResultListElementComponent,
|
||||||
|
PoolMyDSpaceResultListElementComponent,
|
||||||
|
ItemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultDetailElementComponent,
|
||||||
|
ClaimedMyDSpaceResultDetailElementComponent,
|
||||||
|
PoolMyDSpaceResultDetailElementComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module handles all components that are necessary for the mydspace page
|
||||||
|
*/
|
||||||
|
export class MyDSpacePageModule {
|
||||||
|
|
||||||
|
}
|
19
src/app/+my-dspace-page/my-dspace-result.model.ts
Normal file
19
src/app/+my-dspace-page/my-dspace-result.model.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
|
import { MetadataMap } from '../core/shared/metadata.models';
|
||||||
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a search result object of a certain (<T>) DSpaceObject
|
||||||
|
*/
|
||||||
|
export class MyDSpaceResult<T extends DSpaceObject> implements ListableObject {
|
||||||
|
/**
|
||||||
|
* The DSpaceObject that was found
|
||||||
|
*/
|
||||||
|
indexableObject: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata that was used to find this item, hithighlighted
|
||||||
|
*/
|
||||||
|
hitHighlights: MetadataMap;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||||
|
<ds-viewable-collection
|
||||||
|
[config]="searchConfig.pagination"
|
||||||
|
[hasBorder]="hasBorder"
|
||||||
|
[sortConfig]="searchConfig.sort"
|
||||||
|
[objects]="searchResults"
|
||||||
|
[hideGear]="true">
|
||||||
|
</ds-viewable-collection>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
|
||||||
|
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
<h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3>
|
@@ -0,0 +1,58 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-directive-stub';
|
||||||
|
import { MyDSpaceResultsComponent } from './my-dspace-results.component';
|
||||||
|
|
||||||
|
describe('MyDSpaceResultsComponent', () => {
|
||||||
|
let comp: MyDSpaceResultsComponent;
|
||||||
|
let fixture: ComponentFixture<MyDSpaceResultsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
|
||||||
|
declarations: [
|
||||||
|
MyDSpaceResultsComponent,
|
||||||
|
QueryParamsDirectiveStub],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpaceResultsComponent);
|
||||||
|
comp = fixture.componentInstance; // MyDSpaceResultsComponent test instance
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display results when results are not empty', () => {
|
||||||
|
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
|
||||||
|
(comp as any).searchConfig = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display link when results are not empty', () => {
|
||||||
|
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
|
||||||
|
(comp as any).searchConfig = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message if error is != 400', () => {
|
||||||
|
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a message if search result is empty', () => {
|
||||||
|
(comp as any).searchResults = { payload: { page: { length: 0 } } };
|
||||||
|
(comp as any).searchConfig = { query: 'foobar' };
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const linkDes = fixture.debugElement.queryAll(By.css('text-muted'));
|
||||||
|
|
||||||
|
expect(linkDes).toBeDefined()
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { MyDSpaceResult } from '../my-dspace-result.model';
|
||||||
|
import { SearchOptions } from '../../+search-page/search-options.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents all results for mydspace page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-results',
|
||||||
|
templateUrl: './my-dspace-results.component.html',
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MyDSpaceResultsComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual search result objects
|
||||||
|
*/
|
||||||
|
@Input() searchResults: RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current configuration of the search
|
||||||
|
*/
|
||||||
|
@Input() searchConfig: SearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current view mode for the search results
|
||||||
|
*/
|
||||||
|
@Input() viewMode: ViewMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if search results entry are separated by a line
|
||||||
|
*/
|
||||||
|
hasBorder = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mydspace search results are loading
|
||||||
|
*/
|
||||||
|
isLoading() {
|
||||||
|
return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading;
|
||||||
|
}
|
||||||
|
}
|
57
src/app/+my-dspace-page/my-dspace.guard.ts
Normal file
57
src/app/+my-dspace-page/my-dspace.guard.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, NavigationExtras, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { isEmpty } from '../shared/empty.util';
|
||||||
|
import { MYDSPACE_ROUTE } from './my-dspace-page.component';
|
||||||
|
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of mydspace configuration
|
||||||
|
* @class MyDSpaceGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MyDSpaceGuard implements CanActivate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(private configurationService: MyDSpaceConfigurationService, private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when configuration is valid
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
return this.configurationService.getAvailableConfigurationTypes().pipe(
|
||||||
|
first(),
|
||||||
|
map((configurationList) => this.validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given configuration is present in the list of those available
|
||||||
|
*
|
||||||
|
* @param configuration
|
||||||
|
* the configuration to validate
|
||||||
|
* @param configurationList
|
||||||
|
* the list of available configuration
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
|
||||||
|
const configurationDefault: string = configurationList[0];
|
||||||
|
if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) {
|
||||||
|
// If configuration param is empty or is not included in available configurations redirect to a default configuration value
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: {configuration: configurationDefault}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -32,7 +32,7 @@ export class FilteredSearchPageComponent extends SearchPageComponent {
|
|||||||
constructor(protected service: SearchService,
|
constructor(protected service: SearchService,
|
||||||
protected sidebarService: SearchSidebarService,
|
protected sidebarService: SearchSidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService) {
|
||||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
import { MetadataMap } from '../core/shared/metadata.models';
|
import { MetadataMap } from '../core/shared/metadata.models';
|
||||||
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export class NormalizedSearchResult implements ListableObject {
|
|||||||
* The UUID of the DSpaceObject that was found
|
* The UUID of the DSpaceObject that was found
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
dspaceObject: string;
|
indexableObject: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
|
@@ -12,7 +12,7 @@ export class PaginatedSearchOptions extends SearchOptions {
|
|||||||
pagination?: PaginationComponentOptions;
|
pagination?: PaginationComponentOptions;
|
||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
||||||
super(options);
|
super(options);
|
||||||
this.pagination = options.pagination;
|
this.pagination = options.pagination;
|
||||||
this.sort = options.sort;
|
this.sort = options.sort;
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
<div>
|
||||||
|
<div class="filters py-2">
|
||||||
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
||||||
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
|
<div [@facetLoad]="animationState">
|
||||||
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="clearfix toggle-more-filters">
|
||||||
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
|
(click)="showMore()">{{"search.filters.filter.show-more"
|
||||||
|
| translate}}</a>
|
||||||
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
|
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
|
[action]="getCurrentUrl()"
|
||||||
|
[name]="filterConfig.paramName"
|
||||||
|
[(ngModel)]="filter"
|
||||||
|
(submitSuggestion)="onSubmit($event)"
|
||||||
|
(clickSuggestion)="onSubmit($event)"
|
||||||
|
(findSuggestions)="findSuggestions($event)"
|
||||||
|
ngDefaultControl></ds-input-suggestions>
|
||||||
|
</div>
|
@@ -0,0 +1,23 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
a {
|
||||||
|
color: $body-color;
|
||||||
|
&:hover, &focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
span.badge {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-more-filters a {
|
||||||
|
color: $link-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::ng-deep em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
|
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
|
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||||
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-authority-filter',
|
||||||
|
styleUrls: ['./search-authority-filter.component.scss'],
|
||||||
|
templateUrl: './search-authority-filter.component.html',
|
||||||
|
animations: [facetLoad]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents an authority facet for a specific filter configuration
|
||||||
|
*/
|
||||||
|
@renderFacetFor(FilterType.authority)
|
||||||
|
export class SearchAuthorityFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value from search link
|
||||||
|
*/
|
||||||
|
protected getFacetValue(facet: FacetValue): string {
|
||||||
|
const search = facet.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -19,10 +19,12 @@ import { By } from '@angular/platform-browser';
|
|||||||
describe('SearchFacetOptionComponent', () => {
|
describe('SearchFacetOptionComponent', () => {
|
||||||
let comp: SearchFacetOptionComponent;
|
let comp: SearchFacetOptionComponent;
|
||||||
let fixture: ComponentFixture<SearchFacetOptionComponent>;
|
let fixture: ComponentFixture<SearchFacetOptionComponent>;
|
||||||
const filterName1 = 'test name';
|
const filterName1 = 'testname';
|
||||||
|
const filterName2 = 'testAuthorityname';
|
||||||
const value1 = 'testvalue1';
|
const value1 = 'testvalue1';
|
||||||
const value2 = 'test2';
|
const value2 = 'test2';
|
||||||
const value3 = 'another value3';
|
const operator = 'authority';
|
||||||
|
|
||||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
name: filterName1,
|
name: filterName1,
|
||||||
type: FilterType.range,
|
type: FilterType.range,
|
||||||
@@ -32,14 +34,38 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
minValue: 200,
|
minValue: 200,
|
||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName2,
|
||||||
|
type: FilterType.authority,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
|
||||||
const value: FacetValue = {
|
const value: FacetValue = {
|
||||||
value: value2,
|
label: value2,
|
||||||
count: 20,
|
value: value2,
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ``
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedValue: FacetValue = {
|
||||||
|
label: value1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1},${operator}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorityValue: FacetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
const selectedValues = [value1];
|
const selectedValues = [selectedValue];
|
||||||
const selectedValues$ = observableOf(selectedValues);
|
const selectedValues$ = observableOf(selectedValues);
|
||||||
let filterService;
|
let filterService;
|
||||||
let searchService;
|
let searchService;
|
||||||
@@ -90,7 +116,7 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the updateAddParams method is called wih a value', () => {
|
describe('when the updateAddParams method is called with a value', () => {
|
||||||
it('should update the addQueryParams with the new parameter values', () => {
|
it('should update the addQueryParams with the new parameter values', () => {
|
||||||
comp.addQueryParams = {};
|
comp.addQueryParams = {};
|
||||||
(comp as any).updateAddParams(selectedValues);
|
(comp as any).updateAddParams(selectedValues);
|
||||||
@@ -101,6 +127,21 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when filter type is authority and the updateAddParams method is called with a value', () => {
|
||||||
|
it('should update the addQueryParams with the new parameter values', () => {
|
||||||
|
comp.filterValue = authorityValue;
|
||||||
|
comp.filterConfig = mockAuthorityFilterConfig;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
comp.addQueryParams = {};
|
||||||
|
(comp as any).updateAddParams(selectedValues);
|
||||||
|
expect(comp.addQueryParams).toEqual({
|
||||||
|
[mockAuthorityFilterConfig.paramName]: [value1, `${value2},${operator}`],
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when isVisible emits true', () => {
|
describe('when isVisible emits true', () => {
|
||||||
it('the facet option should be visible', () => {
|
it('the facet option should be visible', () => {
|
||||||
comp.isVisible = observableOf(true);
|
comp.isVisible = observableOf(true);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
@@ -8,6 +8,7 @@ import { SearchService } from '../../../../search-service/search.service';
|
|||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
import { hasValue } from '../../../../../shared/empty.util';
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
|
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-option',
|
selector: 'ds-search-facet-option',
|
||||||
@@ -32,7 +33,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
@Input() selectedValues$: Observable<string[]>;
|
@Input() selectedValues$: Observable<FacetValue[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when this option should be visible and false when it should be invisible
|
* Emits true when this option should be visible and false when it should be invisible
|
||||||
@@ -71,7 +72,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Checks if a value for this filter is currently active
|
* Checks if a value for this filter is currently active
|
||||||
*/
|
*/
|
||||||
private isChecked(): Observable<boolean> {
|
private isChecked(): Observable<boolean> {
|
||||||
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
|
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,13 +86,33 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
||||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||||
*/
|
*/
|
||||||
private updateAddParams(selectedValues: string[]): void {
|
private updateAddParams(selectedValues: FacetValue[]): void {
|
||||||
this.addQueryParams = {
|
this.addQueryParams = {
|
||||||
[this.filterConfig.paramName]: [...selectedValues, this.filterValue.value],
|
[this.filterConfig.paramName]: [...selectedValues.map((facetValue: FacetValue) => facetValue.label), this.getFacetValue()],
|
||||||
page: 1
|
page: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value related to facet type
|
||||||
|
*/
|
||||||
|
private getFacetValue(): string {
|
||||||
|
if (this.filterConfig.type === FilterType.authority) {
|
||||||
|
const search = this.filterValue.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
} else {
|
||||||
|
return this.filterValue.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||||
*/
|
*/
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="changeQueryParams" queryParamsHandling="merge">
|
[queryParams]="changeQueryParams" queryParamsHandling="merge">
|
||||||
<span class="filter-value px-1">{{filterValue.value}}</span>
|
<span class="filter-value px-1">{{filterValue.label}}</span>
|
||||||
<span class="float-right filter-value-count ml-auto">
|
<span class="float-right filter-value-count ml-auto">
|
||||||
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -35,10 +35,11 @@ describe('SearchFacetRangeOptionComponent', () => {
|
|||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
const value: FacetValue = {
|
const value: FacetValue = {
|
||||||
value: value2,
|
label: value2,
|
||||||
count: 20,
|
value: value2,
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ''
|
||||||
|
};
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
let filterService;
|
let filterService;
|
||||||
@@ -92,10 +93,11 @@ describe('SearchFacetRangeOptionComponent', () => {
|
|||||||
it('should update the changeQueryParams with the new parameter values', () => {
|
it('should update the changeQueryParams with the new parameter values', () => {
|
||||||
comp.changeQueryParams = {};
|
comp.changeQueryParams = {};
|
||||||
comp.filterValue = {
|
comp.filterValue = {
|
||||||
value: '50-60',
|
label: '50-60',
|
||||||
count: 20,
|
value: '50-60',
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ''
|
||||||
|
};
|
||||||
(comp as any).updateChangeParams();
|
(comp as any).updateChangeParams();
|
||||||
expect(comp.changeQueryParams).toEqual({
|
expect(comp.changeQueryParams).toEqual({
|
||||||
[mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'],
|
[mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'],
|
||||||
|
@@ -2,5 +2,5 @@
|
|||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
||||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||||
<span class="filter-value pl-1">{{selectedValue}}</span>
|
<span class="filter-value pl-1 text-capitalize">{{selectedValue.label}}</span>
|
||||||
</a>
|
</a>
|
@@ -13,13 +13,18 @@ import { RouterStub } from '../../../../../shared/testing/router-stub';
|
|||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
|
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
|
||||||
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
|
|
||||||
describe('SearchFacetSelectedOptionComponent', () => {
|
describe('SearchFacetSelectedOptionComponent', () => {
|
||||||
let comp: SearchFacetSelectedOptionComponent;
|
let comp: SearchFacetSelectedOptionComponent;
|
||||||
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
|
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
|
||||||
const filterName1 = 'test name';
|
const filterName1 = 'test name';
|
||||||
|
const filterName2 = 'testAuthorityname';
|
||||||
|
const label1 = 'test value 1';
|
||||||
const value1 = 'testvalue1';
|
const value1 = 'testvalue1';
|
||||||
|
const label2 = 'test 2';
|
||||||
const value2 = 'test2';
|
const value2 = 'test2';
|
||||||
|
const operator = 'authority';
|
||||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
name: filterName1,
|
name: filterName1,
|
||||||
type: FilterType.range,
|
type: FilterType.range,
|
||||||
@@ -29,10 +34,55 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
minValue: 200,
|
minValue: 200,
|
||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
|
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName2,
|
||||||
|
type: FilterType.authority,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
const selectedValues = [value1, value2];
|
const selectedValue: FacetValue = {
|
||||||
|
label: value1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1}`
|
||||||
|
};
|
||||||
|
const selectedValue2: FacetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value2}`
|
||||||
|
};
|
||||||
|
const selectedAuthorityValue: FacetValue = {
|
||||||
|
label: label1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value1},${operator}`
|
||||||
|
};
|
||||||
|
const selectedAuthorityValue2: FacetValue = {
|
||||||
|
label: label2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
|
const selectedValues = [selectedValue, selectedValue2];
|
||||||
|
const selectedAuthorityValues = [selectedAuthorityValue, selectedAuthorityValue2];
|
||||||
|
const facetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 1,
|
||||||
|
search: ''
|
||||||
|
};
|
||||||
|
const authorityValue: FacetValue = {
|
||||||
|
label: label2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
const selectedValues$ = observableOf(selectedValues);
|
const selectedValues$ = observableOf(selectedValues);
|
||||||
|
const selectedAuthorityValues$ = observableOf(selectedAuthorityValues);
|
||||||
let filterService;
|
let filterService;
|
||||||
let searchService;
|
let searchService;
|
||||||
let router;
|
let router;
|
||||||
@@ -76,7 +126,7 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
filterService = (comp as any).filterService;
|
filterService = (comp as any).filterService;
|
||||||
searchService = (comp as any).searchService;
|
searchService = (comp as any).searchService;
|
||||||
router = (comp as any).router;
|
router = (comp as any).router;
|
||||||
comp.selectedValue = value2;
|
comp.selectedValue = facetValue;
|
||||||
comp.selectedValues$ = selectedValues$;
|
comp.selectedValues$ = selectedValues$;
|
||||||
comp.filterConfig = mockFilterConfig;
|
comp.filterConfig = mockFilterConfig;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -92,4 +142,20 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when filter type is authority and the updateRemoveParams method is called with a value', () => {
|
||||||
|
it('should update the removeQueryParams with the new parameter values', () => {
|
||||||
|
spyOn(filterService, 'getSelectedValuesForFilter').and.returnValue(selectedAuthorityValues);
|
||||||
|
comp.selectedValue = authorityValue;
|
||||||
|
comp.selectedValues$ = selectedAuthorityValues$;
|
||||||
|
comp.filterConfig = mockAuthorityFilterConfig;
|
||||||
|
comp.removeQueryParams = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
(comp as any).updateRemoveParams(selectedAuthorityValues);
|
||||||
|
expect(comp.removeQueryParams).toEqual({
|
||||||
|
[mockAuthorityFilterConfig.paramName]: [`${value1},${operator}`],
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,6 +6,8 @@ import { SearchService } from '../../../../search-service/search.service';
|
|||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { hasValue } from '../../../../../shared/empty.util';
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
|
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-selected-option',
|
selector: 'ds-search-facet-selected-option',
|
||||||
@@ -20,7 +22,7 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The value for this component
|
* The value for this component
|
||||||
*/
|
*/
|
||||||
@Input() selectedValue: string;
|
@Input() selectedValue: FacetValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filter configuration for this facet option
|
* The filter configuration for this facet option
|
||||||
@@ -30,7 +32,7 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
@Input() selectedValues$: Observable<string[]>;
|
@Input() selectedValues$: Observable<FacetValue[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI parameters when this filter is removed
|
* UI parameters when this filter is removed
|
||||||
@@ -70,13 +72,35 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
||||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||||
*/
|
*/
|
||||||
private updateRemoveParams(selectedValues: string[]): void {
|
private updateRemoveParams(selectedValues: FacetValue[]): void {
|
||||||
this.removeQueryParams = {
|
this.removeQueryParams = {
|
||||||
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== this.selectedValue),
|
[this.filterConfig.paramName]: selectedValues
|
||||||
|
.filter((facetValue: FacetValue) => facetValue.label !== this.selectedValue.label)
|
||||||
|
.map((facetValue: FacetValue) => this.getFacetValue(facetValue)),
|
||||||
page: 1
|
page: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value related to facet type
|
||||||
|
*/
|
||||||
|
private getFacetValue(facetValue: FacetValue): string {
|
||||||
|
if (this.filterConfig.type === FilterType.authority) {
|
||||||
|
const search = facetValue.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
} else {
|
||||||
|
return facetValue.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||||
*/
|
*/
|
||||||
|
@@ -17,7 +17,8 @@ import { Router } from '@angular/router';
|
|||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { SearchFacetFilterComponent } from './search-facet-filter.component';
|
import { SearchFacetFilterComponent } from './search-facet-filter.component';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
describe('SearchFacetFilterComponent', () => {
|
describe('SearchFacetFilterComponent', () => {
|
||||||
let comp: SearchFacetFilterComponent;
|
let comp: SearchFacetFilterComponent;
|
||||||
@@ -35,14 +36,17 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
const values: FacetValue[] = [
|
const values: FacetValue[] = [
|
||||||
{
|
{
|
||||||
|
label: value1,
|
||||||
value: value1,
|
value: value1,
|
||||||
count: 52,
|
count: 52,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value2,
|
||||||
value: value2,
|
value: value2,
|
||||||
count: 20,
|
count: 20,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value3,
|
||||||
value: value3,
|
value: value3,
|
||||||
count: 5,
|
count: 5,
|
||||||
search: ''
|
search: ''
|
||||||
@@ -66,7 +70,7 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
||||||
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
||||||
{ provide: SearchConfigurationService, useValue: {searchOptions: observableOf({})} },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
||||||
|
@@ -6,7 +6,7 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators';
|
import { switchMap, distinctUntilChanged, map, take, flatMap } from 'rxjs/operators';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -23,6 +23,7 @@ import { SearchConfigurationService } from '../../../search-service/search-confi
|
|||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
import { SearchOptions } from '../../../search-options.model';
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
@@ -56,7 +57,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List of subscriptions to unsubscribe from
|
* List of subscriptions to unsubscribe from
|
||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
protected subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the result values for this filter found by the current filter query
|
* Emits the result values for this filter found by the current filter query
|
||||||
@@ -66,8 +67,8 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
selectedValues$: Observable<string[]>;
|
selectedValues$: Observable<FacetValue[]>;
|
||||||
private collapseNextUpdate = true;
|
protected collapseNextUpdate = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of the requested facets used to time the animation
|
* State of the requested facets used to time the animation
|
||||||
@@ -81,9 +82,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
|
||||||
protected rdbs: RemoteDataBuildService,
|
protected rdbs: RemoteDataBuildService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +95,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
||||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||||
|
|
||||||
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
|
||||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||||
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
||||||
const facetValues = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
const facetValues$ = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
||||||
map(([options, page]) => {
|
map(([options, page]) => {
|
||||||
return { options, page }
|
return { options, page }
|
||||||
}),
|
}),
|
||||||
@@ -115,8 +115,17 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.selectedValues$ = observableCombineLatest(
|
||||||
|
this.filterService.getSelectedValuesForFilter(this.filterConfig),
|
||||||
|
facetValues$.pipe(flatMap((facetValues) => facetValues.values))).pipe(
|
||||||
|
map(([selectedValues, facetValues]) => {
|
||||||
|
return facetValues.payload.page.filter((facetValue) => selectedValues.includes(this.getFacetValue(facetValue)))
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
let filterValues = [];
|
let filterValues = [];
|
||||||
this.subs.push(facetValues.subscribe((facetOutcome) => {
|
this.subs.push(facetValues$.subscribe((facetOutcome) => {
|
||||||
const newValues$ = facetOutcome.values;
|
const newValues$ = facetOutcome.values;
|
||||||
|
|
||||||
if (this.collapseNextUpdate) {
|
if (this.collapseNextUpdate) {
|
||||||
@@ -201,7 +210,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.router.navigate([this.getSearchLink()], {
|
this.router.navigate([this.getSearchLink()], {
|
||||||
queryParams:
|
queryParams:
|
||||||
{ [this.filterConfig.paramName]: [...selectedValues, data] },
|
{ [this.filterConfig.paramName]: [
|
||||||
|
...selectedValues.map((facet) => this.getFacetValue(facet)),
|
||||||
|
data
|
||||||
|
] },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
@@ -252,7 +264,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
return rd.payload.page.map((facet) => {
|
return rd.payload.page.map((facet) => {
|
||||||
return {
|
return {
|
||||||
displayValue: this.getDisplayValue(facet, data),
|
displayValue: this.getDisplayValue(facet, data),
|
||||||
value: facet.value
|
value: this.getFacetValue(facet)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -264,6 +276,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve facet value
|
||||||
|
*/
|
||||||
|
protected getFacetValue(facet: FacetValue): string {
|
||||||
|
return facet.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
||||||
* @param {FacetValue} facet The value of the facet as returned by the server
|
* @param {FacetValue} facet The value of the facet as returned by the server
|
||||||
|
@@ -11,6 +11,8 @@ import { SearchFilterComponent } from './search-filter.component';
|
|||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../search-service/filter-type.model';
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
@@ -54,8 +56,6 @@ describe('SearchFilterComponent', () => {
|
|||||||
getFacetValuesFor: (filter) => mockResults
|
getFacetValuesFor: (filter) => mockResults
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchConfigServiceStub = {};
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
@@ -66,7 +66,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: mockFilterService
|
useValue: mockFilterService
|
||||||
},
|
},
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFilterComponent, {
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchFilterService } from './search-filter.service';
|
import { SearchFilterService } from './search-filter.service';
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
|
||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { SearchService } from '../../search-service/search.service';
|
import { SearchService } from '../../search-service/search.service';
|
||||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filter',
|
selector: 'ds-search-filter',
|
||||||
@@ -44,7 +47,10 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
active$: Observable<boolean>;
|
active$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
|
constructor(
|
||||||
|
private filterService: SearchFilterService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { SearchFilterAction, SearchFilterActionTypes, SearchFilterInitializeAction } from './search-filter.actions';
|
||||||
SearchFilterAction,
|
|
||||||
SearchFilterActionTypes,
|
|
||||||
SearchFilterInitializeAction
|
|
||||||
} from './search-filter.actions';
|
|
||||||
import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that represents the state for a single filters
|
* Interface that represents the state for a single filters
|
||||||
|
@@ -19,6 +19,8 @@ import { SearchRangeFilterComponent } from './search-range-filter.component';
|
|||||||
import { RouteService } from '../../../../shared/services/route.service';
|
import { RouteService } from '../../../../shared/services/route.service';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchRangeFilterComponent', () => {
|
describe('SearchRangeFilterComponent', () => {
|
||||||
let comp: SearchRangeFilterComponent;
|
let comp: SearchRangeFilterComponent;
|
||||||
@@ -41,14 +43,17 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
const values: FacetValue[] = [
|
const values: FacetValue[] = [
|
||||||
{
|
{
|
||||||
|
label: value1,
|
||||||
value: value1,
|
value: value1,
|
||||||
count: 52,
|
count: 52,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value2,
|
||||||
value: value2,
|
value: value2,
|
||||||
count: 20,
|
count: 20,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value3,
|
||||||
value: value3,
|
value: value3,
|
||||||
count: 5,
|
count: 5,
|
||||||
search: ''
|
search: ''
|
||||||
@@ -73,9 +78,7 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
{ provide: FILTER_CONFIG, useValue: mockFilterConfig },
|
{ provide: FILTER_CONFIG, useValue: mockFilterConfig },
|
||||||
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
||||||
{ provide: RouteService, useValue: {getQueryParameterValue: () => observableOf({})} },
|
{ provide: RouteService, useValue: {getQueryParameterValue: () => observableOf({})} },
|
||||||
{ provide: SearchConfigurationService, useValue: {
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
searchOptions: observableOf({}) }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => selectedValues,
|
getSelectedValuesForFilter: () => selectedValues,
|
||||||
|
@@ -17,6 +17,7 @@ import * as moment from 'moment';
|
|||||||
import { RouteService } from '../../../../shared/services/route.service';
|
import { RouteService } from '../../../../shared/services/route.service';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The suffix for a range filters' minimum in the frontend URL
|
* The suffix for a range filters' minimum in the frontend URL
|
||||||
@@ -72,13 +73,13 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
|
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected rdbs: RemoteDataBuildService,
|
protected rdbs: RemoteDataBuildService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
private route: RouteService) {
|
private route: RouteService) {
|
||||||
super(searchService, filterService, searchConfigService, rdbs, router, filterConfig);
|
super(searchService, filterService, rdbs, router, searchConfigService, filterConfig);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,13 +7,15 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { SearchFiltersComponent } from './search-filters.component';
|
import { SearchFiltersComponent } from './search-filters.component';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchFiltersComponent', () => {
|
describe('SearchFiltersComponent', () => {
|
||||||
let comp: SearchFiltersComponent;
|
let comp: SearchFiltersComponent;
|
||||||
let fixture: ComponentFixture<SearchFiltersComponent>;
|
let fixture: ComponentFixture<SearchFiltersComponent>;
|
||||||
let searchService: SearchService;
|
let searchService: SearchService;
|
||||||
|
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
getConfig: () =>
|
getConfig: () =>
|
||||||
@@ -30,17 +32,13 @@ describe('SearchFiltersComponent', () => {
|
|||||||
[]
|
[]
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchConfigServiceStub = jasmine.createSpyObj('SearchConfigurationService', {
|
|
||||||
getCurrentFrontendFilters: observableOf({})
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
declarations: [SearchFiltersComponent],
|
declarations: [SearchFiltersComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: SearchFilterService, useValue: searchFiltersStub },
|
{ provide: SearchFilterService, useValue: searchFiltersStub },
|
||||||
|
|
||||||
],
|
],
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filters',
|
selector: 'ds-search-filters',
|
||||||
@@ -18,7 +20,7 @@ import { getSucceededRemoteData } from '../../core/shared/operators';
|
|||||||
/**
|
/**
|
||||||
* This component represents the part of the search sidebar that contains filters.
|
* This component represents the part of the search sidebar that contains filters.
|
||||||
*/
|
*/
|
||||||
export class SearchFiltersComponent {
|
export class SearchFiltersComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* An observable containing configuration about which filters are shown and how they are shown
|
* An observable containing configuration about which filters are shown and how they are shown
|
||||||
*/
|
*/
|
||||||
@@ -36,9 +38,20 @@ export class SearchFiltersComponent {
|
|||||||
* @param {SearchConfigurationService} searchConfigService
|
* @param {SearchConfigurationService} searchConfigService
|
||||||
* @param {SearchFilterService} filterService
|
* @param {SearchFilterService} filterService
|
||||||
*/
|
*/
|
||||||
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService, private filterService: SearchFilterService) {
|
constructor(
|
||||||
this.filters = searchService.getConfig().pipe(getSucceededRemoteData());
|
private searchService: SearchService,
|
||||||
this.clearParams = searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
|
private filterService: SearchFilterService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
this.filters = this.searchConfigService.searchOptions.pipe(
|
||||||
|
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getSucceededRemoteData()))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
|
||||||
Object.keys(filters).forEach((f) => filters[f] = null);
|
Object.keys(filters).forEach((f) => filters[f] = null);
|
||||||
return filters;
|
return filters;
|
||||||
}));
|
}));
|
||||||
@@ -57,4 +70,5 @@ export class SearchFiltersComponent {
|
|||||||
trackUpdate(index, config: SearchFilterConfig) {
|
trackUpdate(index, config: SearchFilterConfig) {
|
||||||
return config ? config.name : undefined;
|
return config ? config.name : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,11 @@
|
|||||||
<div class="labels col-sm-9 offset-sm-3">
|
<div class="labels col-sm-9 offset-sm-3">
|
||||||
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)"><!--Do not remove this to prevent uneven spacing
|
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)"><!--Do not remove this to prevent uneven spacing
|
||||||
--><a *ngFor="let values of (appliedFilters | async)[key]"
|
--><a *ngFor="let values of (appliedFilters | async)[key]"
|
||||||
class="badge badge-primary mr-1 mb-1"
|
class="badge badge-primary mr-1 mb-1 text-capitalize"
|
||||||
[routerLink]="getSearchLink()"
|
[routerLink]="getSearchLink()"
|
||||||
[queryParams]="(getRemoveParams(key, values) | async)" queryParamsHandling="merge">
|
[queryParams]="(getRemoveParams(key, values) | async)" queryParamsHandling="merge">
|
||||||
{{('search.filters.applied.' + key) | translate}}: {{values}}
|
{{('search.filters.applied.' + key) | translate}}: {{normalizeFilterValue(values)}}
|
||||||
<span> ×</span>
|
<span> ×</span>
|
||||||
</a><!--Do not remove this to prevent uneven spacing
|
</a><!--Do not remove this to prevent uneven spacing
|
||||||
--></ng-container>
|
--></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -9,7 +9,8 @@ import { SearchServiceStub } from '../../shared/testing/search-service-stub';
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe';
|
import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchLabelsComponent', () => {
|
describe('SearchLabelsComponent', () => {
|
||||||
let comp: SearchLabelsComponent;
|
let comp: SearchLabelsComponent;
|
||||||
@@ -20,8 +21,11 @@ describe('SearchLabelsComponent', () => {
|
|||||||
|
|
||||||
const field1 = 'author';
|
const field1 = 'author';
|
||||||
const field2 = 'subject';
|
const field2 = 'subject';
|
||||||
const value1 = 'TestAuthor';
|
const value1 = 'Test, Author';
|
||||||
|
const normValue1 = 'Test, Author';
|
||||||
const value2 = 'TestSubject';
|
const value2 = 'TestSubject';
|
||||||
|
const value3 = 'Test, Authority,authority';
|
||||||
|
const normValue3 = 'Test, Authority';
|
||||||
const filter1 = [field1, value1];
|
const filter1 = [field1, value1];
|
||||||
const filter2 = [field2, value2];
|
const filter2 = [field2, value2];
|
||||||
const mockFilters = [
|
const mockFilters = [
|
||||||
@@ -35,7 +39,8 @@ describe('SearchLabelsComponent', () => {
|
|||||||
declarations: [SearchLabelsComponent, ObjectKeysPipe],
|
declarations: [SearchLabelsComponent, ObjectKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||||
{ provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }
|
||||||
|
// { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchLabelsComponent, {
|
}).overrideComponent(SearchLabelsComponent, {
|
||||||
@@ -65,4 +70,16 @@ describe('SearchLabelsComponent', () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when normalizeFilterValue is called', () => {
|
||||||
|
it('should return properly filter value', () => {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
result = comp.normalizeFilterValue(value1);
|
||||||
|
expect(result).toBe(normValue1);
|
||||||
|
|
||||||
|
result = comp.normalizeFilterValue(value3);
|
||||||
|
expect(result).toBe(normValue3);
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-labels',
|
selector: 'ds-search-labels',
|
||||||
@@ -24,7 +25,9 @@ export class SearchLabelsComponent {
|
|||||||
/**
|
/**
|
||||||
* Initialize the instance variable
|
* 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();
|
this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,4 +56,17 @@ export class SearchLabelsComponent {
|
|||||||
getSearchLink() {
|
getSearchLink() {
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Strips authority operator from filter value
|
||||||
|
* e.g. 'test ,authority' => 'test'
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
normalizeFilterValue(value: string) {
|
||||||
|
// const pattern = /,[^,]*$/g;
|
||||||
|
const pattern = /,authority*$/g;
|
||||||
|
return value.replace(pattern, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import { SetViewMode } from '../shared/view-mode';
|
|||||||
* This model class represents all parameters needed to request information about a certain search request
|
* This model class represents all parameters needed to request information about a certain search request
|
||||||
*/
|
*/
|
||||||
export class SearchOptions {
|
export class SearchOptions {
|
||||||
|
configuration?: string;
|
||||||
view?: SetViewMode = SetViewMode.List;
|
view?: SetViewMode = SetViewMode.List;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
@@ -16,7 +17,8 @@ export class SearchOptions {
|
|||||||
filters?: any;
|
filters?: any;
|
||||||
fixedFilter?: any;
|
fixedFilter?: any;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any}) {
|
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any}) {
|
||||||
|
this.configuration = options.configuration;
|
||||||
this.scope = options.scope;
|
this.scope = options.scope;
|
||||||
this.query = options.query;
|
this.query = options.query;
|
||||||
this.dsoType = options.dsoType;
|
this.dsoType = options.dsoType;
|
||||||
@@ -31,6 +33,9 @@ export class SearchOptions {
|
|||||||
* @returns {string} URL with all search options and passed arguments as query parameters
|
* @returns {string} URL with all search options and passed arguments as query parameters
|
||||||
*/
|
*/
|
||||||
toRestUrl(url: string, args: string[] = []): string {
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
|
if (isNotEmpty(this.configuration)) {
|
||||||
|
args.push(`configuration=${this.configuration}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(this.fixedFilter)) {
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
args.push(this.fixedFilter);
|
args.push(this.fixedFilter);
|
||||||
}
|
}
|
||||||
@@ -45,7 +50,10 @@ export class SearchOptions {
|
|||||||
}
|
}
|
||||||
if (isNotEmpty(this.filters)) {
|
if (isNotEmpty(this.filters)) {
|
||||||
this.filters.forEach((filter: SearchFilter) => {
|
this.filters.forEach((filter: SearchFilter) => {
|
||||||
filter.values.forEach((value) => args.push(`${filter.key}=${value},${filter.operator}`));
|
filter.values.forEach((value) => {
|
||||||
|
const filterValue = value.includes(',') ? `${value}` : `${value},${filter.operator}`;
|
||||||
|
args.push(`${filter.key}=${filterValue}`)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
|
@@ -20,11 +20,15 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
import { RouteService } from '../shared/services/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
|
||||||
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
|
|
||||||
let comp: SearchPageComponent;
|
let comp: SearchPageComponent;
|
||||||
let fixture: ComponentFixture<SearchPageComponent>;
|
let fixture: ComponentFixture<SearchPageComponent>;
|
||||||
let searchServiceObject: SearchService;
|
let searchServiceObject: SearchService;
|
||||||
|
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
dispatch: {},
|
dispatch: {},
|
||||||
@@ -42,17 +46,25 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
|||||||
getSearchLink: '/search',
|
getSearchLink: '/search',
|
||||||
getScopes: observableOf(['test-scope'])
|
getScopes: observableOf(['test-scope'])
|
||||||
});
|
});
|
||||||
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
const fixedFilter = 'fixed filter';
|
const fixedFilter = 'fixed filter';
|
||||||
const paginatedSearchOptions = {
|
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||||
|
configuration: configurationParam,
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam,
|
scope: scopeParam,
|
||||||
fixedFilter: fixedFilter,
|
fixedFilter: fixedFilter,
|
||||||
pagination,
|
pagination,
|
||||||
sort
|
sort
|
||||||
};
|
});
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
|
snapshot: {
|
||||||
|
queryParamMap: new Map([
|
||||||
|
['query', queryParam],
|
||||||
|
['scope', scopeParam]
|
||||||
|
])
|
||||||
|
},
|
||||||
queryParams: observableOf({
|
queryParams: observableOf({
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam
|
scope: scopeParam
|
||||||
@@ -81,6 +93,7 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
||||||
},
|
},
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{
|
{
|
||||||
provide: Store, useValue: store
|
provide: Store, useValue: store
|
||||||
},
|
},
|
||||||
@@ -114,9 +127,9 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: RouteService,
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
useValue: routeServiceStub
|
useValue: new SearchConfigurationServiceStub()
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(compType, {
|
}).overrideComponent(compType, {
|
||||||
@@ -125,7 +138,6 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('SearchPageComponent', () => {
|
describe('SearchPageComponent', () => {
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
configureSearchComponentTestingModule(SearchPageComponent);
|
configureSearchComponentTestingModule(SearchPageComponent);
|
||||||
}));
|
}));
|
||||||
@@ -135,25 +147,21 @@ describe('SearchPageComponent', () => {
|
|||||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
searchServiceObject = (comp as any).service;
|
searchServiceObject = (comp as any).service;
|
||||||
|
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
searchServiceObject = null;
|
||||||
|
searchConfigurationServiceObject = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the scope and query from the route parameters', () => {
|
it('should get the scope and query from the route parameters', () => {
|
||||||
|
|
||||||
|
searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions);
|
||||||
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||||
b: paginatedSearchOptions
|
b: paginatedSearchOptions
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the closeSidebar event is emitted clicked in mobile view', () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(comp, 'closeSidebar');
|
|
||||||
const closeSidebarButton = fixture.debugElement.query(By.css('#search-sidebar-sm'));
|
|
||||||
closeSidebarButton.triggerEventHandler('toggleSidebar', null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger the closeSidebar function', () => {
|
|
||||||
expect(comp.closeSidebar).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
||||||
import { switchMap, } from 'rxjs/operators';
|
import { switchMap, } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -7,7 +7,6 @@ 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 { PaginatedSearchOptions } from './paginated-search-options.model';
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
|
||||||
import { SearchResult } from './search-result.model';
|
import { SearchResult } from './search-result.model';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
@@ -15,13 +14,28 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
|||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../shared/services/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
|
export const SEARCH_ROUTE = '/search';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-page',
|
selector: 'ds-search-page',
|
||||||
styleUrls: ['./search-page.component.scss'],
|
styleUrls: ['./search-page.component.scss'],
|
||||||
templateUrl: './search-page.component.html',
|
templateUrl: './search-page.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [pushInOut]
|
animations: [pushInOut],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +90,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
constructor(protected service: SearchService,
|
constructor(protected service: SearchService,
|
||||||
protected sidebarService: SearchSidebarService,
|
protected sidebarService: SearchSidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
|
@@ -5,9 +5,6 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||||
import { ItemSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
|
|
||||||
import { CollectionSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
|
|
||||||
import { CommunitySearchResultListElementComponent } from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
|
||||||
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
|
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
|
||||||
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
||||||
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||||
@@ -33,11 +30,39 @@ import { SearchConfigurationService } from './search-service/search-configuratio
|
|||||||
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
|
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
|
||||||
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
|
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
|
||||||
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
|
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
|
||||||
|
import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component';
|
||||||
|
import { SearchAuthorityFilterComponent } from './search-filters/search-filter/search-authority-filter/search-authority-filter.component';
|
||||||
|
|
||||||
const effects = [
|
const effects = [
|
||||||
SearchSidebarEffects
|
SearchSidebarEffects
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
SearchPageComponent,
|
||||||
|
SearchResultsComponent,
|
||||||
|
SearchSidebarComponent,
|
||||||
|
SearchSettingsComponent,
|
||||||
|
ItemSearchResultGridElementComponent,
|
||||||
|
CollectionSearchResultGridElementComponent,
|
||||||
|
CommunitySearchResultGridElementComponent,
|
||||||
|
SearchFiltersComponent,
|
||||||
|
SearchFilterComponent,
|
||||||
|
SearchFacetFilterComponent,
|
||||||
|
SearchLabelsComponent,
|
||||||
|
SearchFacetFilterComponent,
|
||||||
|
SearchFacetFilterWrapperComponent,
|
||||||
|
SearchRangeFilterComponent,
|
||||||
|
SearchTextFilterComponent,
|
||||||
|
SearchHierarchyFilterComponent,
|
||||||
|
SearchBooleanFilterComponent,
|
||||||
|
SearchFacetOptionComponent,
|
||||||
|
SearchFacetSelectedOptionComponent,
|
||||||
|
SearchFacetRangeOptionComponent,
|
||||||
|
SearchSwitchConfigurationComponent,
|
||||||
|
SearchAuthorityFilterComponent,
|
||||||
|
FilteredSearchPageComponent
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SearchPageRoutingModule,
|
SearchPageRoutingModule,
|
||||||
@@ -46,29 +71,7 @@ const effects = [
|
|||||||
EffectsModule.forFeature(effects),
|
EffectsModule.forFeature(effects),
|
||||||
CoreModule.forRoot()
|
CoreModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: components,
|
||||||
SearchPageComponent,
|
|
||||||
FilteredSearchPageComponent,
|
|
||||||
SearchResultsComponent,
|
|
||||||
SearchSidebarComponent,
|
|
||||||
SearchSettingsComponent,
|
|
||||||
ItemSearchResultGridElementComponent,
|
|
||||||
CollectionSearchResultGridElementComponent,
|
|
||||||
CommunitySearchResultGridElementComponent,
|
|
||||||
SearchFiltersComponent,
|
|
||||||
SearchFilterComponent,
|
|
||||||
SearchFacetFilterComponent,
|
|
||||||
SearchLabelsComponent,
|
|
||||||
SearchFacetFilterComponent,
|
|
||||||
SearchFacetFilterWrapperComponent,
|
|
||||||
SearchRangeFilterComponent,
|
|
||||||
SearchTextFilterComponent,
|
|
||||||
SearchHierarchyFilterComponent,
|
|
||||||
SearchBooleanFilterComponent,
|
|
||||||
SearchFacetOptionComponent,
|
|
||||||
SearchFacetSelectedOptionComponent,
|
|
||||||
SearchFacetRangeOptionComponent
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
SearchSidebarService,
|
SearchSidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
@@ -78,9 +81,6 @@ const effects = [
|
|||||||
SearchConfigurationService
|
SearchConfigurationService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
ItemSearchResultListElementComponent,
|
|
||||||
CollectionSearchResultListElementComponent,
|
|
||||||
CommunitySearchResultListElementComponent,
|
|
||||||
ItemSearchResultGridElementComponent,
|
ItemSearchResultGridElementComponent,
|
||||||
CollectionSearchResultGridElementComponent,
|
CollectionSearchResultGridElementComponent,
|
||||||
CommunitySearchResultGridElementComponent,
|
CommunitySearchResultGridElementComponent,
|
||||||
@@ -91,11 +91,10 @@ const effects = [
|
|||||||
SearchBooleanFilterComponent,
|
SearchBooleanFilterComponent,
|
||||||
SearchFacetOptionComponent,
|
SearchFacetOptionComponent,
|
||||||
SearchFacetSelectedOptionComponent,
|
SearchFacetSelectedOptionComponent,
|
||||||
SearchFacetRangeOptionComponent
|
SearchFacetRangeOptionComponent,
|
||||||
|
SearchAuthorityFilterComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: components
|
||||||
FilteredSearchPageComponent,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,7 +9,7 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The DSpaceObject that was found
|
* The DSpaceObject that was found
|
||||||
*/
|
*/
|
||||||
dspaceObject: T;
|
indexableObject: T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
|
@@ -5,7 +5,13 @@ import { autoserialize, autoserializeAs } from 'cerialize';
|
|||||||
*/
|
*/
|
||||||
export class FacetValue {
|
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')
|
@autoserializeAs(String, 'label')
|
||||||
value: string;
|
value: string;
|
||||||
|
@@ -2,6 +2,11 @@
|
|||||||
* Enumeration containing all possible types for filters
|
* Enumeration containing all possible types for filters
|
||||||
*/
|
*/
|
||||||
export enum FilterType {
|
export enum FilterType {
|
||||||
|
/**
|
||||||
|
* Represents authority facets
|
||||||
|
*/
|
||||||
|
authority = 'authority',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents simple text facets
|
* Represents simple text facets
|
||||||
*/
|
*/
|
||||||
|
@@ -17,6 +17,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
const defaults = new PaginatedSearchOptions({
|
const defaults = new PaginatedSearchOptions({
|
||||||
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
|
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
|
||||||
sort: new SortOptions('score', SortDirection.DESC),
|
sort: new SortOptions('score', SortDirection.DESC),
|
||||||
|
configuration: 'default',
|
||||||
query: '',
|
query: '',
|
||||||
scope: ''
|
scope: ''
|
||||||
});
|
});
|
||||||
@@ -47,6 +48,15 @@ describe('SearchConfigurationService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentConfiguration is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentConfiguration('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'configuration\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('configuration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when getCurrentQuery is called', () => {
|
describe('when getCurrentQuery is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.getCurrentQuery('');
|
service.getCurrentQuery('');
|
||||||
@@ -98,6 +108,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getCurrentPagination is called', () => {
|
describe('when getCurrentPagination is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
||||||
@@ -109,11 +120,13 @@ describe('SearchConfigurationService', () => {
|
|||||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when subscribeToSearchOptions or subscribeToPaginatedSearchOptions is called', () => {
|
describe('when subscribeToSearchOptions or subscribeToPaginatedSearchOptions is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service, 'getCurrentPagination').and.callThrough();
|
spyOn(service, 'getCurrentPagination').and.callThrough();
|
||||||
spyOn(service, 'getCurrentSort').and.callThrough();
|
spyOn(service, 'getCurrentSort').and.callThrough();
|
||||||
spyOn(service, 'getCurrentScope').and.callThrough();
|
spyOn(service, 'getCurrentScope').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentConfiguration').and.callThrough();
|
||||||
spyOn(service, 'getCurrentQuery').and.callThrough();
|
spyOn(service, 'getCurrentQuery').and.callThrough();
|
||||||
spyOn(service, 'getCurrentDSOType').and.callThrough();
|
spyOn(service, 'getCurrentDSOType').and.callThrough();
|
||||||
spyOn(service, 'getCurrentFilters').and.callThrough();
|
spyOn(service, 'getCurrentFilters').and.callThrough();
|
||||||
@@ -127,6 +140,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
||||||
expect(service.getCurrentSort).not.toHaveBeenCalled();
|
expect(service.getCurrentSort).not.toHaveBeenCalled();
|
||||||
expect(service.getCurrentScope).toHaveBeenCalled();
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
expect(service.getCurrentQuery).toHaveBeenCalled();
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
expect(service.getCurrentFilters).toHaveBeenCalled();
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
@@ -141,6 +155,7 @@ describe('SearchConfigurationService', () => {
|
|||||||
expect(service.getCurrentPagination).toHaveBeenCalled();
|
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||||
expect(service.getCurrentSort).toHaveBeenCalled();
|
expect(service.getCurrentSort).toHaveBeenCalled();
|
||||||
expect(service.getCurrentScope).toHaveBeenCalled();
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
expect(service.getCurrentQuery).toHaveBeenCalled();
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
expect(service.getCurrentFilters).toHaveBeenCalled();
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
@@ -10,9 +13,7 @@ import { filter, flatMap, map } from 'rxjs/operators';
|
|||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
|
||||||
import { RouteService } from '../../shared/services/route.service';
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
@@ -29,7 +30,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Default pagination settings
|
* Default pagination settings
|
||||||
*/
|
*/
|
||||||
private defaultPagination = Object.assign(new PaginationComponentOptions(), {
|
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'search-page-configuration',
|
id: 'search-page-configuration',
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
currentPage: 1
|
currentPage: 1
|
||||||
@@ -38,22 +39,27 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Default sort settings
|
* 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
|
* Default scope setting
|
||||||
*/
|
*/
|
||||||
private defaultScope = '';
|
protected defaultScope = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default query setting
|
* Default query setting
|
||||||
*/
|
*/
|
||||||
private defaultQuery = '';
|
protected defaultQuery = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the current default values
|
* Emits the current default values
|
||||||
*/
|
*/
|
||||||
private _defaults: Observable<RemoteData<PaginatedSearchOptions>>;
|
protected _defaults: Observable<RemoteData<PaginatedSearchOptions>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the current search options
|
* Emits the current search options
|
||||||
@@ -68,7 +74,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List of subscriptions to unsubscribe from on destroy
|
* List of subscriptions to unsubscribe from on destroy
|
||||||
*/
|
*/
|
||||||
private subs: Subscription[] = new Array();
|
protected subs: Subscription[] = new Array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the search options
|
* Initialize the search options
|
||||||
@@ -76,22 +82,40 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
* @param {SearchFixedFilterService} fixedFilterService
|
* @param {SearchFixedFilterService} fixedFilterService
|
||||||
* @param {ActivatedRoute} route
|
* @param {ActivatedRoute} route
|
||||||
*/
|
*/
|
||||||
constructor(private routeService: RouteService,
|
constructor(protected routeService: RouteService,
|
||||||
private fixedFilterService: SearchFixedFilterService,
|
protected fixedFilterService: SearchFixedFilterService,
|
||||||
private route: ActivatedRoute) {
|
protected route: ActivatedRoute) {
|
||||||
|
|
||||||
|
this.initDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the search options
|
||||||
|
*/
|
||||||
|
protected initDefaults() {
|
||||||
this.defaults
|
this.defaults
|
||||||
.pipe(getSucceededRemoteData())
|
.pipe(getSucceededRemoteData())
|
||||||
.subscribe((defRD) => {
|
.subscribe((defRD) => {
|
||||||
const defs = defRD.payload;
|
const defs = defRD.payload;
|
||||||
this.paginatedSearchOptions = new BehaviorSubject<SearchOptions>(defs);
|
this.paginatedSearchOptions = new BehaviorSubject<PaginatedSearchOptions>(defs);
|
||||||
this.searchOptions = new BehaviorSubject<PaginatedSearchOptions>(defs);
|
this.searchOptions = new BehaviorSubject<SearchOptions>(defs);
|
||||||
|
|
||||||
this.subs.push(this.subscribeToSearchOptions(defs));
|
this.subs.push(this.subscribeToSearchOptions(defs));
|
||||||
this.subs.push(this.subscribeToPaginatedSearchOptions(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
|
* @returns {Observable<string>} Emits the current scope's identifier
|
||||||
*/
|
*/
|
||||||
@@ -115,7 +139,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
getCurrentDSOType(): Observable<DSpaceObjectType> {
|
getCurrentDSOType(): Observable<DSpaceObjectType> {
|
||||||
return this.routeService.getQueryParameterValue('dsoType').pipe(
|
return this.routeService.getQueryParameterValue('dsoType').pipe(
|
||||||
filter((type) => hasValue(type) && hasValue(DSpaceObjectType[type.toUpperCase()])),
|
filter((type) => isNotEmpty(type) && hasValue(DSpaceObjectType[type.toUpperCase()])),
|
||||||
map((type) => DSpaceObjectType[type.toUpperCase()]),);
|
map((type) => DSpaceObjectType[type.toUpperCase()]),);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +224,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
|
||||||
return observableMerge(
|
return observableMerge(
|
||||||
|
this.getConfigurationPart(defaults.configuration),
|
||||||
this.getScopePart(defaults.scope),
|
this.getScopePart(defaults.scope),
|
||||||
this.getQueryPart(defaults.query),
|
this.getQueryPart(defaults.query),
|
||||||
this.getDSOTypePart(),
|
this.getDSOTypePart(),
|
||||||
@@ -221,6 +246,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
return observableMerge(
|
return observableMerge(
|
||||||
this.getPaginationPart(defaults.pagination),
|
this.getPaginationPart(defaults.pagination),
|
||||||
this.getSortPart(defaults.sort),
|
this.getSortPart(defaults.sort),
|
||||||
|
this.getConfigurationPart(defaults.configuration),
|
||||||
this.getScopePart(defaults.scope),
|
this.getScopePart(defaults.scope),
|
||||||
this.getQueryPart(defaults.query),
|
this.getQueryPart(defaults.query),
|
||||||
this.getDSOTypePart(),
|
this.getDSOTypePart(),
|
||||||
@@ -240,6 +266,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
if (hasNoValue(this._defaults)) {
|
if (hasNoValue(this._defaults)) {
|
||||||
const options = new PaginatedSearchOptions({
|
const options = new PaginatedSearchOptions({
|
||||||
pagination: this.defaultPagination,
|
pagination: this.defaultPagination,
|
||||||
|
configuration: this.defaultConfiguration,
|
||||||
sort: this.defaultSort,
|
sort: this.defaultSort,
|
||||||
scope: this.defaultScope,
|
scope: this.defaultScope,
|
||||||
query: this.defaultQuery
|
query: this.defaultQuery
|
||||||
@@ -256,6 +283,16 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
this.subs.forEach((sub) => {
|
this.subs.forEach((sub) => {
|
||||||
sub.unsubscribe();
|
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 }
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -34,7 +34,7 @@ export class SearchQueryResponse {
|
|||||||
* The sort parameters used in the search request
|
* The sort parameters used in the search request
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
configurationName: string;
|
configuration: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sort parameters used in the search request
|
* The sort parameters used in the search request
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
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
|
* 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
|
* @param {GenericConstructor<ListableObject>} domainConstructor The constructor of the DSpaceObject
|
||||||
* @returns Decorator function that performs the actual mapping on initialization of the component
|
* @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) {
|
return function decorator(searchResult: any) {
|
||||||
if (!searchResult) {
|
if (!searchResult) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
searchResultMap.set(domainConstructor, searchResult);
|
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
|
* @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
|
* @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) {
|
||||||
return searchResultMap.get(domainConstructor);
|
if (isNull(configuration) || configuration === 'default') {
|
||||||
|
return searchResultMap.get(domainConstructor);
|
||||||
|
} else {
|
||||||
|
return searchResultMap.get(configuration).get(domainConstructor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,27 +9,25 @@ import { ItemDataService } from './../../core/data/item-data.service';
|
|||||||
import { SetViewMode } from '../../shared/view-mode';
|
import { SetViewMode } from '../../shared/view-mode';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
import { RouterStub } from '../../shared/testing/router-stub';
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { RequestEntry } from '../../core/data/request.reducer';
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import {
|
import { FacetConfigSuccessResponse, SearchSuccessResponse } from '../../core/cache/response.models';
|
||||||
FacetConfigSuccessResponse,
|
|
||||||
SearchSuccessResponse
|
|
||||||
} from '../../core/cache/response.models';
|
|
||||||
import { SearchQueryResponse } from './search-query-response.model';
|
import { SearchQueryResponse } from './search-query-response.model';
|
||||||
import { SearchFilterConfig } from './search-filter-config.model';
|
import { SearchFilterConfig } from './search-filter-config.model';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { routeServiceStub } from '../../shared/testing/route-service-stub';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent {
|
class DummyComponent {
|
||||||
@@ -53,7 +51,7 @@ describe('SearchService', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
@@ -74,7 +72,7 @@ describe('SearchService', () => {
|
|||||||
describe('', () => {
|
describe('', () => {
|
||||||
let searchService: SearchService;
|
let searchService: SearchService;
|
||||||
const router = new RouterStub();
|
const router = new RouterStub();
|
||||||
const route = new ActivatedRouteStub();
|
let routeService;
|
||||||
|
|
||||||
const halService = {
|
const halService = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
@@ -110,7 +108,7 @@ describe('SearchService', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||||
{ provide: HALEndpointService, useValue: halService },
|
{ provide: HALEndpointService, useValue: halService },
|
||||||
@@ -120,6 +118,7 @@ describe('SearchService', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
searchService = TestBed.get(SearchService);
|
searchService = TestBed.get(SearchService);
|
||||||
|
routeService = TestBed.get(RouteService);
|
||||||
const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } });
|
const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } });
|
||||||
router.parseUrl.and.returnValue(urlTree);
|
router.parseUrl.and.returnValue(urlTree);
|
||||||
});
|
});
|
||||||
@@ -127,7 +126,7 @@ describe('SearchService', () => {
|
|||||||
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
||||||
searchService.setViewMode(ViewMode.List);
|
searchService.setViewMode(ViewMode.List);
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
queryParams: { view: ViewMode.List },
|
queryParams: { view: ViewMode.List, page: 1 },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -135,21 +134,26 @@ describe('SearchService', () => {
|
|||||||
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
||||||
searchService.setViewMode(ViewMode.Grid);
|
searchService.setViewMode(ViewMode.Grid);
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
queryParams: { view: ViewMode.Grid },
|
queryParams: { view: ViewMode.Grid, page: 1 },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
||||||
let viewMode = ViewMode.Grid;
|
let viewMode = ViewMode.Grid;
|
||||||
route.testParams = { view: ViewMode.List };
|
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
|
||||||
|
[ 'view', ViewMode.List ],
|
||||||
|
])));
|
||||||
|
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
expect(viewMode).toEqual(ViewMode.List);
|
expect(viewMode).toEqual(ViewMode.List);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
|
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
|
||||||
let viewMode = ViewMode.List;
|
let viewMode = ViewMode.List;
|
||||||
route.testParams = { view: ViewMode.Grid };
|
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
|
||||||
|
[ 'view', ViewMode.Grid ],
|
||||||
|
])));
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
expect(viewMode).toEqual(ViewMode.Grid);
|
expect(viewMode).toEqual(ViewMode.Grid);
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import {
|
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
||||||
ActivatedRoute,
|
import { first, map, switchMap } from 'rxjs/operators';
|
||||||
NavigationExtras,
|
|
||||||
PRIMARY_OUTLET,
|
|
||||||
Router,
|
|
||||||
UrlSegmentGroup
|
|
||||||
} from '@angular/router';
|
|
||||||
import { map, switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
@@ -23,12 +17,13 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
|||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import {
|
import {
|
||||||
configureRequest, filterSuccessfulResponses,
|
configureRequest,
|
||||||
|
filterSuccessfulResponses,
|
||||||
getResponseFromEntry,
|
getResponseFromEntry,
|
||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
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 { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
@@ -47,6 +42,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all general actions that have to do with the search page
|
* Service that performs all general actions that have to do with the search page
|
||||||
@@ -63,13 +59,23 @@ export class SearchService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private facetLinkPathPrefix = 'discover/facets/';
|
private facetLinkPathPrefix = 'discover/facets/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ResponseParsingService constructor name
|
||||||
|
*/
|
||||||
|
private parser: GenericConstructor<ResponseParsingService> = SearchResponseParsingService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RestRequest constructor name
|
||||||
|
*/
|
||||||
|
private request: GenericConstructor<RestRequest> = GetRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to unsubscribe from
|
* Subscription to unsubscribe from
|
||||||
*/
|
*/
|
||||||
private sub;
|
private sub;
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute,
|
private routeService: RouteService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
@@ -78,6 +84,20 @@ export class SearchService implements OnDestroy {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to set service options
|
||||||
|
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
|
||||||
|
* @param {boolean} request The RestRequest constructor name
|
||||||
|
*/
|
||||||
|
setServiceOptions(parser: GenericConstructor<ResponseParsingService>, request: GenericConstructor<RestRequest>) {
|
||||||
|
if (parser) {
|
||||||
|
this.parser = parser;
|
||||||
|
}
|
||||||
|
if (request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to retrieve a paginated list of search results from the server
|
* Method to retrieve a paginated list of search results from the server
|
||||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||||
@@ -89,14 +109,17 @@ export class SearchService implements OnDestroy {
|
|||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||||
}
|
}
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
|
||||||
|
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||||
|
return this.parser;
|
||||||
|
};
|
||||||
|
|
||||||
return Object.assign(request, {
|
return Object.assign(request, {
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser: getResponseParserFn
|
||||||
return SearchResponseParsingService;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
configureRequest(this.requestService)
|
configureRequest(this.requestService),
|
||||||
);
|
);
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
@@ -112,8 +135,10 @@ export class SearchService implements OnDestroy {
|
|||||||
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
||||||
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
||||||
map((sqr: SearchQueryResponse) => {
|
map((sqr: SearchQueryResponse) => {
|
||||||
return sqr.objects.map((nsr: NormalizedSearchResult) => {
|
return sqr.objects
|
||||||
return this.rdb.buildSingle(nsr.dspaceObject);
|
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject))
|
||||||
|
.map((nsr: NormalizedSearchResult) => {
|
||||||
|
return this.rdb.buildSingle(nsr.indexableObject);
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||||
@@ -126,9 +151,9 @@ export class SearchService implements OnDestroy {
|
|||||||
let co = DSpaceObject;
|
let co = DSpaceObject;
|
||||||
if (dsos.payload[index]) {
|
if (dsos.payload[index]) {
|
||||||
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
|
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, {
|
return Object.assign(new co(), object, {
|
||||||
dspaceObject: dsos.payload[index]
|
indexableObject: dsos.payload[index]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -154,9 +179,10 @@ export class SearchService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Request the filter configuration for a given scope or the whole repository
|
* 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} 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
|
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
|
||||||
*/
|
*/
|
||||||
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||||
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
||||||
map((url: string) => {
|
map((url: string) => {
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
@@ -165,11 +191,15 @@ export class SearchService implements OnDestroy {
|
|||||||
args.push(`scope=${scope}`);
|
args.push(`scope=${scope}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(configurationName)) {
|
||||||
|
args.push(`configuration=${configurationName}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
return Object.assign(request, {
|
return Object.assign(request, {
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
return FacetConfigResponseParsingService;
|
return FacetConfigResponseParsingService;
|
||||||
@@ -212,14 +242,15 @@ export class SearchService implements OnDestroy {
|
|||||||
url = searchOptions.toRestUrl(url, args);
|
url = searchOptions.toRestUrl(url, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
return Object.assign(request, {
|
return Object.assign(request, {
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
return FacetValueResponseParsingService;
|
return FacetValueResponseParsingService;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
configureRequest(this.requestService)
|
configureRequest(this.requestService),
|
||||||
|
first()
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
@@ -288,9 +319,9 @@ export class SearchService implements OnDestroy {
|
|||||||
* @returns {Observable<ViewMode>} The current view mode
|
* @returns {Observable<ViewMode>} The current view mode
|
||||||
*/
|
*/
|
||||||
getViewMode(): Observable<ViewMode> {
|
getViewMode(): Observable<ViewMode> {
|
||||||
return this.route.queryParams.pipe(map((params) => {
|
return this.routeService.getQueryParamMap().pipe(map((params) => {
|
||||||
if (isNotEmpty(params.view) && hasValue(params.view)) {
|
if (isNotEmpty(params.get('view')) && hasValue(params.get('view'))) {
|
||||||
return params.view;
|
return params.get('view');
|
||||||
} else {
|
} else {
|
||||||
return ViewMode.List;
|
return ViewMode.List;
|
||||||
}
|
}
|
||||||
@@ -302,12 +333,21 @@ export class SearchService implements OnDestroy {
|
|||||||
* @param {ViewMode} viewMode Mode to switch to
|
* @param {ViewMode} viewMode Mode to switch to
|
||||||
*/
|
*/
|
||||||
setViewMode(viewMode: ViewMode) {
|
setViewMode(viewMode: ViewMode) {
|
||||||
const navigationExtras: NavigationExtras = {
|
this.routeService.getQueryParameterValue('pageSize').pipe(first())
|
||||||
queryParams: { view: viewMode },
|
.subscribe((pageSize) => {
|
||||||
queryParamsHandling: 'merge'
|
let queryParams = { view: viewMode, page: 1 };
|
||||||
};
|
if (viewMode === ViewMode.Detail) {
|
||||||
|
queryParams = Object.assign(queryParams, {pageSize: '1'});
|
||||||
|
} else if (pageSize === '1') {
|
||||||
|
queryParams = Object.assign(queryParams, {pageSize: '10'});
|
||||||
|
}
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: queryParams,
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
this.router.navigate([this.getSearchLink()], navigationExtras);
|
this.router.navigate([this.getSearchLink()], navigationExtras);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,7 +356,8 @@ export class SearchService implements OnDestroy {
|
|||||||
getSearchLink(): string {
|
getSearchLink(): string {
|
||||||
const urlTree = this.router.parseUrl(this.router.url);
|
const urlTree = this.router.parseUrl(this.router.url);
|
||||||
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||||
return '/' + g.toString();
|
const searchLink: any = '/' + g.toString();
|
||||||
|
return (searchLink !== '/search' && searchLink !== '/mydspace') ? '/search' : searchLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -14,8 +14,8 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
|
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
|
||||||
import { hot } from 'jasmine-marbles';
|
import { hot } from 'jasmine-marbles';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
describe('SearchSettingsComponent', () => {
|
describe('SearchSettingsComponent', () => {
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ describe('SearchSettingsComponent', () => {
|
|||||||
useValue: {}
|
useValue: {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: SearchConfigurationService,
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
useValue: {
|
useValue: {
|
||||||
paginatedSearchOptions: hot('a', {
|
paginatedSearchOptions: hot('a', {
|
||||||
a: paginatedSearchOptions
|
a: paginatedSearchOptions
|
||||||
|
@@ -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 { SearchService } from '../search-service/search.service';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-settings',
|
selector: 'ds-search-settings',
|
||||||
@@ -30,7 +31,7 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private searchConfigurationService: SearchConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -8,8 +8,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-sidebar-content">
|
<div id="search-sidebar-content">
|
||||||
<ds-view-mode-switch class="d-none d-md-block"></ds-view-mode-switch>
|
<ds-view-mode-switch [viewModeList]="viewModeList" class="d-none d-md-block"></ds-view-mode-switch>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
|
<ds-search-switch-configuration *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
|
||||||
<ds-search-filters></ds-search-filters>
|
<ds-search-filters></ds-search-filters>
|
||||||
<ds-search-settings></ds-search-settings>
|
<ds-search-settings></ds-search-settings>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,8 +8,12 @@
|
|||||||
ds-view-mode-switch {
|
ds-view-mode-switch {
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
}
|
}
|
||||||
.sidebar-content > *:not(:last-child) {
|
.sidebar-content > *:not(:last-child):not(ds-search-switch-configuration) {
|
||||||
margin-bottom: 4*$spacer;
|
margin-bottom: 4*$spacer;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
ds-search-switch-configuration {
|
||||||
|
margin-bottom: 2*$spacer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
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.
|
* This component renders a simple item page.
|
||||||
* The route parameter 'id' is used to request the item it represents.
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
@@ -17,11 +19,21 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
*/
|
*/
|
||||||
export class SearchSidebarComponent {
|
export class SearchSidebarComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available configuration options
|
||||||
|
*/
|
||||||
|
@Input() configurationList: SearchConfigurationOption[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total amount of results
|
* The total amount of results
|
||||||
*/
|
*/
|
||||||
@Input() resultCount;
|
@Input() resultCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available view mode options
|
||||||
|
*/
|
||||||
|
@Input() viewModeList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits event when the user clicks a button to open or close the sidebar
|
* Emits event when the user clicks a button to open or close the sidebar
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Represents a search configuration select option
|
||||||
|
*/
|
||||||
|
export interface SearchConfigurationOption {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The select option value
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The select option label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
}
|
@@ -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()">
|
||||||
|
<option *ngFor="let option of configurationList;" [ngValue]="option.value">
|
||||||
|
{{option.label | translate}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,109 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SearchSwitchConfigurationComponent } from './search-switch-configuration.component';
|
||||||
|
import { MYDSPACE_ROUTE, SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
|
||||||
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { NavigationExtras, Router } from '@angular/router';
|
||||||
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
|
import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type';
|
||||||
|
import { SearchService } from '../search-service/search.service';
|
||||||
|
|
||||||
|
describe('SearchSwitchConfigurationComponent', () => {
|
||||||
|
|
||||||
|
let comp: SearchSwitchConfigurationComponent;
|
||||||
|
let fixture: ComponentFixture<SearchSwitchConfigurationComponent>;
|
||||||
|
let searchConfService: SearchConfigurationServiceStub;
|
||||||
|
let select: any;
|
||||||
|
|
||||||
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
|
getSearchLink: jasmine.createSpy('getSearchLink')
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [ SearchSwitchConfigurationComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
|
],
|
||||||
|
schemas: [ NO_ERRORS_SCHEMA ]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchSwitchConfigurationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
searchConfService = TestBed.get(SEARCH_CONFIG_SERVICE);
|
||||||
|
|
||||||
|
spyOn(searchConfService, 'getCurrentConfiguration').and.returnValue(observableOf(MyDSpaceConfigurationValueType.Workspace));
|
||||||
|
|
||||||
|
comp.configurationList = [
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
label: 'workspace'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workflow,
|
||||||
|
label: 'workflow'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// SearchSwitchConfigurationComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init the current configuration name', () => {
|
||||||
|
expect(comp.selectedOption).toBe(MyDSpaceConfigurationValueType.Workspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display select field properly', () => {
|
||||||
|
const selectField = fixture.debugElement.query(By.css('.form-control'));
|
||||||
|
expect(selectField).toBeDefined();
|
||||||
|
|
||||||
|
const childElements = selectField.children;
|
||||||
|
expect(childElements.length).toEqual(comp.configurationList.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSelect method when selecting an option', () => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
spyOn(comp, 'onSelect');
|
||||||
|
select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const selectEl = select.nativeElement;
|
||||||
|
selectEl.value = selectEl.options[1].value; // <-- select a new value
|
||||||
|
selectEl.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(comp.onSelect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the route when selecting an option', () => {
|
||||||
|
(comp as any).searchService.getSearchLink.and.returnValue(MYDSPACE_ROUTE);
|
||||||
|
comp.selectedOption = MyDSpaceConfigurationValueType.Workflow;
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: {configuration: MyDSpaceConfigurationValueType.Workflow},
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
comp.onSelect();
|
||||||
|
|
||||||
|
expect((comp as any).router.navigate).toHaveBeenCalledWith([MYDSPACE_ROUTE], navigationExtras);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,80 @@
|
|||||||
|
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 { 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';
|
||||||
|
import { SearchService } from '../search-service/search.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-switch-configuration',
|
||||||
|
styleUrls: ['./search-switch-configuration.component.scss'],
|
||||||
|
templateUrl: './search-switch-configuration.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Represents a select that allow to switch over available search configurations
|
||||||
|
*/
|
||||||
|
export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available configuration options
|
||||||
|
*/
|
||||||
|
@Input() configurationList: SearchConfigurationOption[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected option
|
||||||
|
*/
|
||||||
|
public selectedOption: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from
|
||||||
|
*/
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
constructor(private router: Router,
|
||||||
|
private searchService: SearchService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init current configuration
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.searchConfigService.getCurrentConfiguration('default')
|
||||||
|
.subscribe((currentConfiguration) => this.selectedOption = currentConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init current configuration
|
||||||
|
*/
|
||||||
|
onSelect() {
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: {configuration: this.selectedOption},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.router.navigate([this.searchService.getSearchLink()], navigationExtras);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the select 'compareWith' method to tell Angular how to compare the values
|
||||||
|
*
|
||||||
|
* @param item1
|
||||||
|
* @param item2
|
||||||
|
*/
|
||||||
|
compare(item1: MyDSpaceConfigurationValueType, item2: MyDSpaceConfigurationValueType) {
|
||||||
|
return item1 === item2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -24,6 +24,7 @@ export function getCommunityModulePath() {
|
|||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ 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: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
||||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
|
@@ -6,18 +6,10 @@ import { RequestService } from '../data/request.service';
|
|||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import {
|
import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models';
|
||||||
AuthGetRequest,
|
|
||||||
AuthPostRequest,
|
|
||||||
GetRequest,
|
|
||||||
PostRequest,
|
|
||||||
RestRequest
|
|
||||||
} from '../data/request.models';
|
|
||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
|
@@ -14,7 +14,6 @@ import { AuthType } from './auth-type';
|
|||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
@@ -43,7 +43,7 @@ describe('AuthService test', () => {
|
|||||||
pipe: observableOf(true)
|
pipe: observableOf(true)
|
||||||
});
|
});
|
||||||
window = new NativeWindowRef();
|
window = new NativeWindowRef();
|
||||||
routerStub = new RouterStub()
|
routerStub = new RouterStub();
|
||||||
token = new AuthTokenInfo('test_token');
|
token = new AuthTokenInfo('test_token');
|
||||||
token.expires = Date.now() + (1000 * 60 * 60);
|
token.expires = Date.now() + (1000 * 60 * 60);
|
||||||
authenticatedState = {
|
authenticatedState = {
|
||||||
|
@@ -1,43 +1,27 @@
|
|||||||
import {Observable, of, of as observableOf} from 'rxjs';
|
|
||||||
import {
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
first,
|
|
||||||
map,
|
|
||||||
startWith,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
withLatestFrom
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import { Inject, Injectable, Optional } from '@angular/core';
|
import { Inject, Injectable, Optional } from '@angular/core';
|
||||||
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
|
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
|
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||||
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { CookieService } from '../../shared/services/cookie.service';
|
import { CookieService } from '../../shared/services/cookie.service';
|
||||||
import {
|
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||||
getAuthenticationToken,
|
|
||||||
getRedirectUrl,
|
|
||||||
isAuthenticated,
|
|
||||||
isTokenRefreshing
|
|
||||||
} from './selectors';
|
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
|
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
|
@@ -8,6 +8,7 @@ import { createSelector } from '@ngrx/store';
|
|||||||
*/
|
*/
|
||||||
import { AuthState } from './auth.reducer';
|
import { AuthState } from './auth.reducer';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user state.
|
* Returns the user state.
|
||||||
@@ -35,11 +36,12 @@ const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the users state
|
* Return the users state
|
||||||
|
* NOTE: when state is REHYDRATED user object lose prototype so return always a new EPerson object
|
||||||
* @function _getAuthenticatedUser
|
* @function _getAuthenticatedUser
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {User}
|
* @returns {EPerson}
|
||||||
*/
|
*/
|
||||||
const _getAuthenticatedUser = (state: AuthState) => state.user;
|
const _getAuthenticatedUser = (state: AuthState) => Object.assign(new EPerson(), state.user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication error.
|
* Returns the authentication error.
|
||||||
|
@@ -15,13 +15,14 @@ import { NormalizedWorkspaceItem } from '../../submission/models/normalized-work
|
|||||||
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
|
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
|
||||||
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
|
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
|
||||||
import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model';
|
import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model';
|
||||||
|
import { NormalizedClaimedTask } from '../../tasks/models/normalized-claimed-task-object.model';
|
||||||
|
import { NormalizedPoolTask } from '../../tasks/models/normalized-pool-task-object.model';
|
||||||
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
||||||
import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model';
|
import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model';
|
||||||
import { CacheableObject } from '../object-cache.reducer';
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model';
|
import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model';
|
||||||
import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model';
|
import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model';
|
||||||
import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model';
|
import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model';
|
||||||
import { NormalizedAuthStatus } from '../../auth/models/normalized-auth-status.model';
|
|
||||||
|
|
||||||
export class NormalizedObjectFactory {
|
export class NormalizedObjectFactory {
|
||||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject<CacheableObject>> {
|
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject<CacheableObject>> {
|
||||||
@@ -77,6 +78,12 @@ export class NormalizedObjectFactory {
|
|||||||
case ResourceType.Workflowitem: {
|
case ResourceType.Workflowitem: {
|
||||||
return NormalizedWorkflowItem
|
return NormalizedWorkflowItem
|
||||||
}
|
}
|
||||||
|
case ResourceType.ClaimedTask: {
|
||||||
|
return NormalizedClaimedTask
|
||||||
|
}
|
||||||
|
case ResourceType.PoolTask: {
|
||||||
|
return NormalizedPoolTask
|
||||||
|
}
|
||||||
case ResourceType.SubmissionDefinition:
|
case ResourceType.SubmissionDefinition:
|
||||||
case ResourceType.SubmissionDefinitions: {
|
case ResourceType.SubmissionDefinitions: {
|
||||||
return NormalizedSubmissionDefinitionsModel
|
return NormalizedSubmissionDefinitionsModel
|
||||||
|
@@ -1,18 +1,19 @@
|
|||||||
|
import * as ngrx from '@ngrx/store';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import {
|
import {
|
||||||
AddPatchObjectCacheAction,
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction, ApplyPatchObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
|
ApplyPatchObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction
|
RemoveFromObjectCacheAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
import { NormalizedItem } from './models/normalized-item.model';
|
import { NormalizedItem } from './models/normalized-item.model';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import * as ngrx from '@ngrx/store';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { Operation } from '../../../../node_modules/fast-json-patch';
|
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { AddToSSBAction } from './server-sync-buffer.actions';
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
import { Patch } from './object-cache.reducer';
|
import { Patch } from './object-cache.reducer';
|
||||||
|
2
src/app/core/cache/object-cache.service.ts
vendored
2
src/app/core/cache/object-cache.service.ts
vendored
@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
25
src/app/core/cache/response.models.ts
vendored
25
src/app/core/cache/response.models.ts
vendored
@@ -8,7 +8,6 @@ import { IntegrationModel } from '../integration/models/integration.model';
|
|||||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
|
||||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||||
import { MetadataField } from '../metadata/metadatafield.model';
|
import { MetadataField } from '../metadata/metadatafield.model';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
@@ -255,6 +254,30 @@ export class EpersonSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MessageResponse extends RestResponse {
|
||||||
|
public toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskResponse extends RestResponse {
|
||||||
|
public toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FilteredDiscoveryQueryResponse extends RestResponse {
|
export class FilteredDiscoveryQueryResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public filterQuery: string,
|
public filterQuery: string,
|
||||||
|
@@ -81,6 +81,12 @@ import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
|||||||
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||||
import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service';
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { RoleService } from './roles/role.service';
|
||||||
|
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
|
||||||
|
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
||||||
|
import { ClaimedTaskDataService } from './tasks/claimed-task-data.service';
|
||||||
|
import { PoolTaskDataService } from './tasks/pool-task-data.service';
|
||||||
|
import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -132,6 +138,7 @@ const PROVIDERS = [
|
|||||||
RegistryBitstreamformatsResponseParsingService,
|
RegistryBitstreamformatsResponseParsingService,
|
||||||
DebugResponseParsingService,
|
DebugResponseParsingService,
|
||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
|
MyDSpaceResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
BrowseEntriesResponseParsingService,
|
BrowseEntriesResponseParsingService,
|
||||||
@@ -163,6 +170,11 @@ const PROVIDERS = [
|
|||||||
MenuService,
|
MenuService,
|
||||||
ObjectUpdatesService,
|
ObjectUpdatesService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
MyDSpaceGuard,
|
||||||
|
RoleService,
|
||||||
|
TaskResponseParsingService,
|
||||||
|
ClaimedTaskDataService,
|
||||||
|
PoolTaskDataService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -6,9 +6,8 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
|||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|
||||||
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service';
|
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
export abstract class BaseResponseParsingService {
|
export abstract class BaseResponseParsingService {
|
||||||
@@ -142,4 +141,8 @@ export abstract class BaseResponseParsingService {
|
|||||||
protected retrieveObjectOrUrl(obj: any): any {
|
protected retrieveObjectOrUrl(obj: any): any {
|
||||||
return this.toCache ? obj.self : obj;
|
return this.toCache ? obj.self : obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isSuccessStatus(statusCode: number) {
|
||||||
|
return statusCode >= 200 && statusCode < 300;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
@@ -13,6 +15,10 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FindAllOptions } from './request.models';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends ComColDataService<Collection> {
|
export class CollectionDataService extends ComColDataService<Collection> {
|
||||||
@@ -34,4 +40,21 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find whether there is a collection whom user has authorization to submit to
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
* true if the user has at least one collection to submit to
|
||||||
|
*/
|
||||||
|
hasAuthorizedCollection(): Observable<boolean> {
|
||||||
|
const searchHref = 'findAuthorized';
|
||||||
|
const options = new FindAllOptions();
|
||||||
|
options.elementsPerPage = 1;
|
||||||
|
|
||||||
|
return this.searchBy(searchHref, options).pipe(
|
||||||
|
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending),
|
||||||
|
take(1),
|
||||||
|
map((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.totalElements > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { filter, mergeMap, take } from 'rxjs/operators';
|
import { filter, take } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
@@ -12,7 +11,7 @@ import { RequestService } from './request.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { FindAllOptions, FindAllRequest } from './request.models';
|
import { FindAllOptions, FindAllRequest } from './request.models';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
@@ -9,13 +9,12 @@ import { Observable, of as observableOf } from 'rxjs';
|
|||||||
import { FindAllOptions } from './request.models';
|
import { FindAllOptions } from './request.models';
|
||||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Operation } from '../../../../node_modules/fast-json-patch';
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { compare } from 'fast-json-patch';
|
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import { Operation } from 'fast-json-patch/lib/core';
|
import { Operation } from 'fast-json-patch/lib/core';
|
||||||
import { compare } from 'fast-json-patch';
|
import { compare } from 'fast-json-patch';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
|
||||||
|
82
src/app/core/data/mydspace-response-parsing.service.ts
Normal file
82
src/app/core/data/mydspace-response-parsing.service.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RestResponse, SearchSuccessResponse } from '../cache/response.models';
|
||||||
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
|
import { MetadataMap, MetadataValue } from '../shared/metadata.models';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MyDSpaceResponseParsingService implements ResponseParsingService {
|
||||||
|
constructor(private dsoParser: DSOResponseParsingService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
// fallback for unexpected empty response
|
||||||
|
const emptyPayload = {
|
||||||
|
_embedded: {
|
||||||
|
objects: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const payload = data.payload._embedded.searchResult || emptyPayload;
|
||||||
|
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||||
|
.map((object) => object.hitHighlights)
|
||||||
|
.map((hhObject) => {
|
||||||
|
const mdMap: MetadataMap = {};
|
||||||
|
if (hhObject) {
|
||||||
|
for (const key of Object.keys(hhObject)) {
|
||||||
|
const value: MetadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: hhObject[key].join('...'),
|
||||||
|
language: null
|
||||||
|
});
|
||||||
|
mdMap[key] = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mdMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dsoSelfLinks = payload._embedded.objects
|
||||||
|
.filter((object) => hasValue(object._embedded))
|
||||||
|
.map((object) => object._embedded.indexableObject)
|
||||||
|
.map((dso) => this.dsoParser.parse(request, {
|
||||||
|
payload: dso,
|
||||||
|
statusCode: data.statusCode,
|
||||||
|
statusText: data.statusText
|
||||||
|
}))
|
||||||
|
.map((obj) => obj.resourceSelfLinks)
|
||||||
|
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
|
||||||
|
|
||||||
|
const objects = payload._embedded.objects
|
||||||
|
.filter((object) => hasValue(object._embedded))
|
||||||
|
.map((object, index) => Object.assign({}, object, {
|
||||||
|
indexableObject: dsoSelfLinks[index],
|
||||||
|
hitHighlights: hitHighlights[index],
|
||||||
|
_embedded: this.filterEmbeddedObjects(object)
|
||||||
|
}));
|
||||||
|
payload.objects = objects;
|
||||||
|
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
|
||||||
|
return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterEmbeddedObjects(object) {
|
||||||
|
const allowedEmbeddedKeys = ['submitter', 'item', 'workspaceitem', 'workflowitem'];
|
||||||
|
if (object._embedded.indexableObject && object._embedded.indexableObject._embedded) {
|
||||||
|
return Object.assign({}, object._embedded, {
|
||||||
|
indexableObject: Object.assign({}, object._embedded.indexableObject, {
|
||||||
|
_embedded: Object.keys(object._embedded.indexableObject._embedded)
|
||||||
|
.filter((key) => allowedEmbeddedKeys.includes(key))
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = object._embedded.indexableObject._embedded[key];
|
||||||
|
return obj;
|
||||||
|
}, {})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,8 @@ import { coreSelector } from '../../core.selectors';
|
|||||||
import {
|
import {
|
||||||
FieldState,
|
FieldState,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
Identifiable, OBJECT_UPDATES_TRASH_PATH,
|
Identifiable,
|
||||||
|
OBJECT_UPDATES_TRASH_PATH,
|
||||||
ObjectUpdatesEntry,
|
ObjectUpdatesEntry,
|
||||||
ObjectUpdatesState
|
ObjectUpdatesState
|
||||||
} from './object-updates.reducer';
|
} from './object-updates.reducer';
|
||||||
@@ -17,9 +18,10 @@ import {
|
|||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
SetEditableFieldUpdateAction,
|
||||||
|
SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
|
||||||
@@ -212,6 +214,7 @@ export class ObjectUpdatesService {
|
|||||||
/**
|
/**
|
||||||
* Method to dispatch an RemoveFieldUpdateAction to the store
|
* Method to dispatch an RemoveFieldUpdateAction to the store
|
||||||
* @param url The page's URL for which the changes should be removed
|
* @param url The page's URL for which the changes should be removed
|
||||||
|
* @param uuid The UUID of the field that should be set
|
||||||
*/
|
*/
|
||||||
removeSingleFieldUpdate(url: string, uuid) {
|
removeSingleFieldUpdate(url: string, uuid) {
|
||||||
this.store.dispatch(new RemoveFieldUpdateAction(url, uuid));
|
this.store.dispatch(new RemoveFieldUpdateAction(url, uuid));
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
export class PaginatedList<T> {
|
export class PaginatedList<T> {
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export class PaginatedList<T> {
|
|||||||
if (hasValue(this.pageInfo) && hasValue(this.pageInfo.elementsPerPage)) {
|
if (hasValue(this.pageInfo) && hasValue(this.pageInfo.elementsPerPage)) {
|
||||||
return this.pageInfo.elementsPerPage;
|
return this.pageInfo.elementsPerPage;
|
||||||
}
|
}
|
||||||
return this.page.length;
|
return this.getPageLength();
|
||||||
}
|
}
|
||||||
|
|
||||||
set elementsPerPage(value: number) {
|
set elementsPerPage(value: number) {
|
||||||
@@ -22,10 +22,7 @@ export class PaginatedList<T> {
|
|||||||
if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) {
|
if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) {
|
||||||
return this.pageInfo.totalElements;
|
return this.pageInfo.totalElements;
|
||||||
}
|
}
|
||||||
if (hasNoValue(this.page)) {
|
return this.getPageLength();
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return this.page.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set totalElements(value: number) {
|
set totalElements(value: number) {
|
||||||
@@ -92,4 +89,8 @@ export class PaginatedList<T> {
|
|||||||
set self(self: string) {
|
set self(self: string) {
|
||||||
this.pageInfo.self = self;
|
this.pageInfo.self = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getPageLength() {
|
||||||
|
return (Array.isArray(this.page)) ? this.page.length : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,10 +14,10 @@ import { RestRequestMethod } from './rest-request-method';
|
|||||||
import { SearchParam } from '../cache/models/search-param.model';
|
import { SearchParam } from '../cache/models/search-param.model';
|
||||||
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
||||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||||
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
|
|
||||||
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
||||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -371,6 +371,30 @@ export class DeleteByIDRequest extends DeleteRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TaskPostRequest extends PostRequest {
|
||||||
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return TaskResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskDeleteRequest extends DeleteRequest {
|
||||||
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return TaskResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MyDSpaceRequest extends GetRequest {
|
||||||
|
public responseMsToLive = 0;
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable, race as observableRace } from 'rxjs';
|
import { Observable, race as observableRace } from 'rxjs';
|
||||||
import { filter, map, mergeMap, take } from 'rxjs/operators';
|
import { filter, find, map, mergeMap, take } from 'rxjs/operators';
|
||||||
|
import { cloneDeep, remove } from 'lodash';
|
||||||
|
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -23,8 +25,6 @@ import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
|||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base selector function to select the request state in the store
|
* The base selector function to select the request state in the store
|
||||||
@@ -49,7 +49,6 @@ const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, Reques
|
|||||||
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
||||||
* contains a given substring
|
* contains a given substring
|
||||||
* @param selector MemoizedSelector to start from
|
* @param selector MemoizedSelector to start from
|
||||||
* @param name The name of the index substate we're fetching request UUIDs from
|
|
||||||
* @param href Substring that the request's href should contain
|
* @param href Substring that the request's href should contain
|
||||||
*/
|
*/
|
||||||
const uuidsFromHrefSubstringSelector =
|
const uuidsFromHrefSubstringSelector =
|
||||||
@@ -149,12 +148,14 @@ export class RequestService {
|
|||||||
* @param {RestRequest} request The request to send out
|
* @param {RestRequest} request The request to send out
|
||||||
* @param {boolean} forceBypassCache When true, a new request is always dispatched
|
* @param {boolean} forceBypassCache When true, a new request is always dispatched
|
||||||
*/
|
*/
|
||||||
// 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 {
|
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
|
||||||
const isGetRequest = request.method === RestRequestMethod.GET;
|
const isGetRequest = request.method === RestRequestMethod.GET;
|
||||||
if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) {
|
if (forceBypassCache) {
|
||||||
|
this.clearRequestsOnTheirWayToTheStore(request);
|
||||||
|
}
|
||||||
|
if (!isGetRequest || (forceBypassCache && !this.isPending(request)) || !this.isCachedOrPending(request)) {
|
||||||
this.dispatchRequest(request);
|
this.dispatchRequest(request);
|
||||||
if (isGetRequest && !forceBypassCache) {
|
if (isGetRequest) {
|
||||||
this.trackRequestsOnTheirWayToTheStore(request);
|
this.trackRequestsOnTheirWayToTheStore(request);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -168,6 +169,29 @@ export class RequestService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert request Payload to a URL-encoded string
|
||||||
|
*
|
||||||
|
* e.g. uriEncodeBody({param: value, param1: value1})
|
||||||
|
* returns: param=value¶m1=value1
|
||||||
|
*
|
||||||
|
* @param body
|
||||||
|
* The request Payload to convert
|
||||||
|
* @return string
|
||||||
|
* URL-encoded string
|
||||||
|
*/
|
||||||
|
public uriEncodeBody(body: any) {
|
||||||
|
let queryParams = '';
|
||||||
|
if (isNotEmpty(body) && typeof body === 'object') {
|
||||||
|
Object.keys(body)
|
||||||
|
.forEach((param) => {
|
||||||
|
const paramValue = `${param}=${body[param]}`;
|
||||||
|
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return encodeURI(queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all request cache providing (part of) the href
|
* Remove all request cache providing (part of) the href
|
||||||
* This also includes href-to-uuid index cache
|
* This also includes href-to-uuid index cache
|
||||||
@@ -234,6 +258,19 @@ export class RequestService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method remove requests that are on their way to the store.
|
||||||
|
*/
|
||||||
|
private clearRequestsOnTheirWayToTheStore(request: GetRequest) {
|
||||||
|
this.getByHref(request.href).pipe(
|
||||||
|
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)
|
* 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
|
* @param {RestRequestMethod} method RestRequestMethod for which the changes should be committed
|
||||||
|
@@ -15,7 +15,13 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const payload = data.payload._embedded.searchResult;
|
// fallback for unexpected empty response
|
||||||
|
const emptyPayload = {
|
||||||
|
_embedded : {
|
||||||
|
objects: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const payload = data.payload._embedded.searchResult || emptyPayload;
|
||||||
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||||
.map((object) => object.hitHighlights)
|
.map((object) => object.hitHighlights)
|
||||||
.map((hhObject) => {
|
.map((hhObject) => {
|
||||||
@@ -47,7 +53,7 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
|||||||
const objects = payload._embedded.objects
|
const objects = payload._embedded.objects
|
||||||
.filter((object) => hasValue(object._embedded))
|
.filter((object) => hasValue(object._embedded))
|
||||||
.map((object, index) => Object.assign({}, object, {
|
.map((object, index) => Object.assign({}, object, {
|
||||||
dspaceObject: dsoSelfLinks[index],
|
indexableObject: dsoSelfLinks[index],
|
||||||
hitHighlights: hitHighlights[index],
|
hitHighlights: hitHighlights[index],
|
||||||
// we don't need embedded collections, bitstreamformats, etc for search results.
|
// we don't need embedded collections, bitstreamformats, etc for search results.
|
||||||
// And parsing them all takes up a lot of time. Throw them away to improve performance
|
// And parsing them all takes up a lot of time. Throw them away to improve performance
|
||||||
|
@@ -68,6 +68,8 @@ export class DSpaceRESTv2Service {
|
|||||||
* the URL for the request
|
* the URL for the request
|
||||||
* @param body
|
* @param body
|
||||||
* an optional body for the request
|
* an optional body for the request
|
||||||
|
* @param options
|
||||||
|
* the HttpOptions object
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
|
@@ -47,7 +47,9 @@ export class EPerson extends DSpaceObject {
|
|||||||
*/
|
*/
|
||||||
public selfRegistered: boolean;
|
public selfRegistered: boolean;
|
||||||
|
|
||||||
/** Getter to retrieve the EPerson's full name as a string */
|
/**
|
||||||
|
* Getter to retrieve the EPerson's full name as a string
|
||||||
|
*/
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.firstMetadataValue('eperson.firstname') + ' ' + this.firstMetadataValue('eperson.lastname');
|
return this.firstMetadataValue('eperson.firstname') + ' ' + this.firstMetadataValue('eperson.lastname');
|
||||||
}
|
}
|
||||||
|
5
src/app/core/roles/role-types.ts
Normal file
5
src/app/core/roles/role-types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum RoleType {
|
||||||
|
Submitter = 'submitter',
|
||||||
|
Controller = 'controller',
|
||||||
|
Admin = 'admin'
|
||||||
|
}
|
70
src/app/core/roles/role.service.ts
Normal file
70
src/app/core/roles/role.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RoleType } from './role-types';
|
||||||
|
import { CollectionDataService } from '../data/collection-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to identify user role.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RoleService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {CollectionDataService} collectionService
|
||||||
|
*/
|
||||||
|
constructor(private collectionService: CollectionDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is a submitter
|
||||||
|
*/
|
||||||
|
isSubmitter(): Observable<boolean> {
|
||||||
|
return this.collectionService.hasAuthorizedCollection().pipe(
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is a controller
|
||||||
|
*/
|
||||||
|
isController(): Observable<boolean> {
|
||||||
|
// TODO find a way to check if user is a controller
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is an admin
|
||||||
|
*/
|
||||||
|
isAdmin(): Observable<boolean> {
|
||||||
|
// TODO find a way to check if user is an admin
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user by role type
|
||||||
|
*
|
||||||
|
* @param {RoleType} role
|
||||||
|
* the role type
|
||||||
|
*/
|
||||||
|
checkRole(role: RoleType): Observable<boolean> {
|
||||||
|
let check: Observable<boolean>;
|
||||||
|
switch (role) {
|
||||||
|
case RoleType.Submitter:
|
||||||
|
check = this.isSubmitter();
|
||||||
|
break;
|
||||||
|
case RoleType.Controller:
|
||||||
|
check = this.isController();
|
||||||
|
break;
|
||||||
|
case RoleType.Admin:
|
||||||
|
check = this.isAdmin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Collection } from './collection.model';
|
import { Collection } from './collection.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { Relationship } from './item-relationships/relationship.model';
|
import { Relationship } from './item-relationships/relationship.model';
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export class Item extends DSpaceObject {
|
|||||||
*/
|
*/
|
||||||
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||||
return this.bitstreams.pipe(
|
return this.bitstreams.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Bitstream>>) => !rd.isResponsePending),
|
filter((rd: RemoteData<PaginatedList<Bitstream>>) => !rd.isResponsePending && isNotUndefined(rd.payload)),
|
||||||
map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page),
|
map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page),
|
||||||
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
|
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
|
||||||
take(1),
|
take(1),
|
||||||
|
@@ -75,7 +75,7 @@ export const toDSpaceObjectListRD = () =>
|
|||||||
source.pipe(
|
source.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
|
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
|
||||||
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
||||||
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
|
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.indexableObject);
|
||||||
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
|
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
|
||||||
return Object.assign(rd, { payload: payload });
|
return Object.assign(rd, { payload: payload });
|
||||||
})
|
})
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user