mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into main
This commit is contained in:
@@ -165,6 +165,9 @@ languages:
|
|||||||
- code: bn
|
- code: bn
|
||||||
label: বাংলা
|
label: বাংলা
|
||||||
active: true
|
active: true
|
||||||
|
- code: el
|
||||||
|
label: Ελληνικά
|
||||||
|
active: true
|
||||||
|
|
||||||
# Browse-By Pages
|
# Browse-By Pages
|
||||||
browseBy:
|
browseBy:
|
||||||
@@ -174,6 +177,27 @@ browseBy:
|
|||||||
fiveYearLimit: 30
|
fiveYearLimit: 30
|
||||||
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
|
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
|
||||||
defaultLowerLimit: 1900
|
defaultLowerLimit: 1900
|
||||||
|
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
|
||||||
|
showThumbnails: true
|
||||||
|
# The number of entries in a paginated browse results list.
|
||||||
|
# Rounded to the nearest size in the list of selectable sizes on the
|
||||||
|
# settings menu.
|
||||||
|
pageSize: 20
|
||||||
|
|
||||||
|
communityList:
|
||||||
|
# No. of communities to list per expansion (show more)
|
||||||
|
pageSize: 20
|
||||||
|
|
||||||
|
homePage:
|
||||||
|
recentSubmissions:
|
||||||
|
# The number of item showing in recent submission components
|
||||||
|
pageSize: 5
|
||||||
|
# Sort record of recent submission
|
||||||
|
sortField: 'dc.date.accessioned'
|
||||||
|
topLevelCommunityList:
|
||||||
|
# No. of communities to list per page on the home page
|
||||||
|
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
||||||
|
pageSize: 5
|
||||||
|
|
||||||
# Item Config
|
# Item Config
|
||||||
item:
|
item:
|
||||||
@@ -249,7 +273,7 @@ themes:
|
|||||||
|
|
||||||
# The default bundles that should always be displayed as suggestions when you upload a new bundle
|
# The default bundles that should always be displayed as suggestions when you upload a new bundle
|
||||||
bundle:
|
bundle:
|
||||||
- standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
|
standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
|
||||||
|
|
||||||
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
|
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
|
||||||
# For images, this enables a gallery viewer where you can zoom or page through images.
|
# For images, this enables a gallery viewer where you can zoom or page through images.
|
||||||
@@ -264,10 +288,3 @@ mediaViewer:
|
|||||||
info:
|
info:
|
||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
# Home Page
|
|
||||||
homePage:
|
|
||||||
recentSubmissions:
|
|
||||||
# The number of item showing in recent submission components
|
|
||||||
pageSize: 5
|
|
||||||
# Sort record of recent submission
|
|
||||||
sortField: 'dc.date.accessioned'
|
|
||||||
|
@@ -104,6 +104,8 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.10",
|
"klaro": "^0.7.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-mathjax3": "^4.3.1",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.3.0",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.11.0",
|
"mirador-share-plugin": "^0.11.0",
|
||||||
@@ -116,6 +118,7 @@
|
|||||||
"ngx-moment": "^5.0.0",
|
"ngx-moment": "^5.0.0",
|
||||||
"ngx-pagination": "5.0.0",
|
"ngx-pagination": "5.0.0",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
|
"ngx-ui-switch": "^11.0.1",
|
||||||
"nouislider": "^14.6.3",
|
"nouislider": "^14.6.3",
|
||||||
"pem": "1.14.4",
|
"pem": "1.14.4",
|
||||||
"postcss-cli": "^9.1.0",
|
"postcss-cli": "^9.1.0",
|
||||||
@@ -123,13 +126,13 @@
|
|||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.5.5",
|
||||||
|
"sanitize-html": "^2.7.2",
|
||||||
"sortablejs": "1.13.0",
|
"sortablejs": "1.13.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"url-parse": "^1.5.6",
|
"url-parse": "^1.5.6",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "~0.11.5",
|
"zone.js": "~0.11.5"
|
||||||
"ngx-ui-switch": "^11.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~13.1.0",
|
"@angular-builders/custom-webpack": "~13.1.0",
|
||||||
@@ -155,6 +158,7 @@
|
|||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.165",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
|
"@types/sanitize-html": "^2.6.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||||
"@typescript-eslint/parser": "5.11.0",
|
"@typescript-eslint/parser": "5.11.0",
|
||||||
"axe-core": "^4.3.3",
|
"axe-core": "^4.3.3",
|
||||||
|
@@ -76,6 +76,10 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
const server = express();
|
const server = express();
|
||||||
|
|
||||||
|
// Tell Express to trust X-FORWARDED-* headers from proxies
|
||||||
|
// See https://expressjs.com/en/guide/behind-proxies.html
|
||||||
|
server.set('trust proxy', environment.ui.useProxies);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
|
@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||||
if (restResponse.hasSucceeded) {
|
if (restResponse.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
||||||
this.reset();
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
ObservedValueOf,
|
ObservedValueOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
@@ -144,7 +144,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
@@ -153,8 +153,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return epersonDtoModel;
|
return epersonDtoModel;
|
||||||
});
|
});
|
||||||
return dto$;
|
return dto$;
|
||||||
}));
|
})]);
|
||||||
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
}))
|
}))
|
||||||
@@ -174,7 +174,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: 9999
|
elementsPerPage: 9999
|
||||||
}, false)
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
@@ -274,7 +274,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
@@ -283,8 +283,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return epersonDtoModel;
|
return epersonDtoModel;
|
||||||
});
|
});
|
||||||
return dto$;
|
return dto$;
|
||||||
}));
|
})]);
|
||||||
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
}))
|
}))
|
||||||
|
@@ -5,11 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
|
EMPTY,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
@@ -144,7 +145,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
switchMap((isSiteAdmin: boolean) => {
|
switchMap((isSiteAdmin: boolean) => {
|
||||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
return observableCombineLatest([...groups.page.map((group: Group) => {
|
||||||
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||||
return observableCombineLatest([
|
return observableCombineLatest([
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||||
@@ -165,8 +166,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
}
|
}
|
||||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
|
||||||
return buildPaginatedList(groups.pageInfo, dtos);
|
return buildPaginatedList(groups.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
|
||||||
|
<p>{{'admin.batch-import.page.help' | translate}}</p>
|
||||||
|
<p *ngIf="dso">
|
||||||
|
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||||
|
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||||
|
<label class="form-check-label" for="validateOnly">
|
||||||
|
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||||
|
{{'admin.batch-import.page.validateOnly.hint' | translate}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-file-dropzone-no-uploader
|
||||||
|
(onFileAdded)="setFile($event)"
|
||||||
|
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||||
|
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||||
|
</ds-file-dropzone-no-uploader>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
|
<button class="btn btn-secondary" id="backButton"
|
||||||
|
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||||
|
<button class="btn btn-primary" id="proceedButton"
|
||||||
|
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,151 @@
|
|||||||
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { BatchImportPageComponent } from './batch-import-page.component';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
BATCH_IMPORT_SCRIPT_NAME,
|
||||||
|
ScriptDataService
|
||||||
|
} from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
|
||||||
|
describe('BatchImportPageComponent', () => {
|
||||||
|
let component: BatchImportPageComponent;
|
||||||
|
let fixture: ComponentFixture<BatchImportPageComponent>;
|
||||||
|
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let scriptService: any;
|
||||||
|
let router;
|
||||||
|
let locationStub;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService',
|
||||||
|
{
|
||||||
|
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
locationStub = jasmine.createSpyObj('location', {
|
||||||
|
back: jasmine.createSpy('back')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: Location, useValue: locationStub },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BatchImportPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if back button is pressed', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should do location.back', () => {
|
||||||
|
expect(locationStub.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if file is set', () => {
|
||||||
|
let fileMock: File;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
|
||||||
|
component.setFile(fileMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed without validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = false;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
];
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed with validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = true;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed is pressed; but script invoke fails', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('error notification is shown', () => {
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,124 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Process } from '../../process-page/processes/process.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
|
||||||
|
import {
|
||||||
|
ImportBatchSelectorComponent
|
||||||
|
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-batch-import-page',
|
||||||
|
templateUrl: './batch-import-page.component.html'
|
||||||
|
})
|
||||||
|
export class BatchImportPageComponent {
|
||||||
|
/**
|
||||||
|
* The current value of the file
|
||||||
|
*/
|
||||||
|
fileObject: File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validate only flag
|
||||||
|
*/
|
||||||
|
validateOnly = true;
|
||||||
|
/**
|
||||||
|
* dso object for community or collection
|
||||||
|
*/
|
||||||
|
dso: DSpaceObject = null;
|
||||||
|
|
||||||
|
public constructor(private location: Location,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
private scriptDataService: ScriptDataService,
|
||||||
|
private router: Router,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private dsoNameService: DSONameService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set file
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
setFile(file) {
|
||||||
|
this.fileObject = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When return button is pressed go to previous location
|
||||||
|
*/
|
||||||
|
public onReturn() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCollection() {
|
||||||
|
const modalRef = this.modalService.open(ImportBatchSelectorComponent);
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((dso) => {
|
||||||
|
this.dso = dso || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts import-metadata script with --zip fileName (and the selected file)
|
||||||
|
*/
|
||||||
|
public importMetadata() {
|
||||||
|
if (this.fileObject == null) {
|
||||||
|
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||||
|
} else {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' })
|
||||||
|
];
|
||||||
|
if (this.dso) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
|
||||||
|
}
|
||||||
|
if (this.validateOnly) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scriptDataService.invoke(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
const title = this.translate.get('process.new.notification.success.title');
|
||||||
|
const content = this.translate.get('process.new.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content);
|
||||||
|
if (isNotEmpty(rd.payload)) {
|
||||||
|
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const title = this.translate.get('process.new.notification.error.title');
|
||||||
|
const content = this.translate.get('process.new.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return selected dspace object name
|
||||||
|
*/
|
||||||
|
getDspaceObjectName(): string {
|
||||||
|
if (this.dso) {
|
||||||
|
return this.dsoNameService.getName(this.dso);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove selected dso object
|
||||||
|
*/
|
||||||
|
removeDspaceObject(): void {
|
||||||
|
this.dso = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
|||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||||
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
|||||||
component: MetadataImportPageComponent,
|
component: MetadataImportPageComponent,
|
||||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'batch-import',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: BatchImportPageComponent,
|
||||||
|
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -14,6 +14,14 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CollectionAdminSearchResultGridElementComponent;
|
let component: CollectionAdminSearchResultGridElementComponent;
|
||||||
@@ -45,7 +53,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService }
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -16,6 +16,14 @@ import { CommunitySearchResult } from '../../../../../shared/object-collection/s
|
|||||||
import { Community } from '../../../../../core/shared/community.model';
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CommunityAdminSearchResultGridElementComponent;
|
let component: CommunityAdminSearchResultGridElementComponent;
|
||||||
@@ -47,7 +55,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService }
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -20,6 +20,12 @@ import { getMockThemeService } from '../../../../../shared/mocks/theme-service.m
|
|||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||||
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultGridElementComponent;
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
@@ -64,6 +70,9 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
|||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
{ provide: ThemeService, useValue: mockThemeService },
|
{ provide: ThemeService, useValue: mockThemeService },
|
||||||
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
|
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||||
let component: CollectionAdminSearchResultListElementComponent;
|
let component: CollectionAdminSearchResultListElementComponent;
|
||||||
@@ -36,7 +38,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [CollectionAdminSearchResultListElementComponent],
|
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -13,6 +13,8 @@ import { Community } from '../../../../../core/shared/community.model';
|
|||||||
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||||
let component: CommunityAdminSearchResultListElementComponent;
|
let component: CommunityAdminSearchResultListElementComponent;
|
||||||
@@ -36,7 +38,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [CommunityAdminSearchResultListElementComponent],
|
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -10,6 +10,8 @@ import { ItemAdminSearchResultListElementComponent } from './item-admin-search-r
|
|||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultListElementComponent', () => {
|
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultListElementComponent;
|
let component: ItemAdminSearchResultListElementComponent;
|
||||||
@@ -33,7 +35,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [ItemAdminSearchResultListElementComponent],
|
declarations: [ItemAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -18,6 +18,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
|
|||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||||
@@ -51,7 +53,8 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
@@ -13,6 +13,7 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l
|
|||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -32,9 +33,10 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
|
|||||||
|
|
||||||
constructor(private linkService: LinkService,
|
constructor(private linkService: LinkService,
|
||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
protected dsoNameService: DSONameService
|
protected dsoNameService: DSONameService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
) {
|
) {
|
||||||
super(truncatableService, dsoNameService);
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,6 +9,7 @@ import { AdminWorkflowModuleModule } from './admin-workflow-page/admin-workflow.
|
|||||||
import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
||||||
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -28,7 +29,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminCurationTasksComponent,
|
AdminCurationTasksComponent,
|
||||||
MetadataImportPageComponent
|
MetadataImportPageComponent,
|
||||||
|
BatchImportPageComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule {
|
export class AdminModule {
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { CommonModule, DOCUMENT } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
|
||||||
|
|
||||||
// Load the implementations that should be tested
|
// Load the implementations that should be tested
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@@ -73,7 +72,6 @@ describe('App component', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
||||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() },
|
|
||||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
@@ -18,11 +18,10 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
|
|||||||
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
describe('BrowseByDatePageComponent', () => {
|
describe('BrowseByDatePageComponent', () => {
|
||||||
let comp: BrowseByDatePageComponent;
|
let comp: BrowseByDatePageComponent;
|
||||||
@@ -83,7 +82,8 @@ describe('BrowseByDatePageComponent', () => {
|
|||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: ChangeDetectorRef, useValue: mockCdRef }
|
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
browseParamsToOptions
|
browseParamsToOptions, getBrowseSearchOptions
|
||||||
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
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';
|
||||||
@@ -12,13 +11,12 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
|
|||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { isValidDate } from '../../shared/date.util';
|
import { isValidDate } from '../../shared/date.util';
|
||||||
|
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-date-page',
|
selector: 'ds-browse-by-date-page',
|
||||||
@@ -30,7 +28,6 @@ import { isValidDate } from '../../shared/date.util';
|
|||||||
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
|
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
|
||||||
* An example would be 'dateissued' for 'dc.date.issued'
|
* An example would be 'dateissued' for 'dc.date.issued'
|
||||||
*/
|
*/
|
||||||
@rendersBrowseBy(BrowseByDataType.Date)
|
|
||||||
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,14 +40,16 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
protected dsoService: DSpaceObjectDataService,
|
protected dsoService: DSpaceObjectDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected cdRef: ChangeDetectorRef) {
|
protected cdRef: ChangeDetectorRef,
|
||||||
super(route, browseService, dsoService, paginationService, router);
|
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||||
|
super(route, browseService, dsoService, paginationService, router, appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.startsWithType = StartsWithType.date;
|
this.startsWithType = StartsWithType.date;
|
||||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
// include the thumbnail configuration in browse search options
|
||||||
|
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -63,7 +62,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
|
||||||
this.updatePageWithItems(searchOptions, this.value, undefined);
|
this.updatePageWithItems(searchOptions, this.value, undefined);
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
||||||
@@ -83,7 +82,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
|
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
||||||
let lowerLimit = environment.browseBy.defaultLowerLimit;
|
let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
|
||||||
if (hasValue(firstItemRD.payload)) {
|
if (hasValue(firstItemRD.payload)) {
|
||||||
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
|
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
|
||||||
if (isNotEmpty(date) && isValidDate(date)) {
|
if (isNotEmpty(date) && isValidDate(date)) {
|
||||||
@@ -94,8 +93,8 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
}
|
}
|
||||||
const options = [];
|
const options = [];
|
||||||
const currentYear = new Date().getUTCFullYear();
|
const currentYear = new Date().getUTCFullYear();
|
||||||
const oneYearBreak = Math.floor((currentYear - environment.browseBy.oneYearLimit) / 5) * 5;
|
const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||||
const fiveYearBreak = Math.floor((currentYear - environment.browseBy.fiveYearLimit) / 10) * 10;
|
const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||||
if (lowerLimit <= fiveYearBreak) {
|
if (lowerLimit <= fiveYearBreak) {
|
||||||
lowerLimit -= 10;
|
lowerLimit -= 10;
|
||||||
} else if (lowerLimit <= oneYearBreak) {
|
} else if (lowerLimit <= oneYearBreak) {
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
||||||
|
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for BrowseByDatePageComponent
|
||||||
|
* */
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-browse-by-metadata-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
@rendersBrowseBy(BrowseByDataType.Date)
|
||||||
|
export class ThemedBrowseByDatePageComponent
|
||||||
|
extends ThemedComponent<BrowseByDatePageComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'BrowseByDatePageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/browse-by/browse-by-date-page/browse-by-date-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./browse-by-date-page.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -6,10 +6,10 @@
|
|||||||
<ds-comcol-page-header [name]="parentContext.name">
|
<ds-comcol-page-header [name]="parentContext.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-handle
|
<ds-themed-comcol-page-handle
|
||||||
[content]="parentContext.handle"
|
[content]="parentContext.handle"
|
||||||
[title]="parentContext.type+'.page.handle'" >
|
[title]="parentContext.type+'.page.handle'" >
|
||||||
</ds-comcol-page-handle>
|
</ds-themed-comcol-page-handle>
|
||||||
<!-- Introductory text -->
|
<!-- Introductory text -->
|
||||||
<ds-comcol-page-content [content]="parentContext.introductoryText" [hasInnerHtml]="true">
|
<ds-comcol-page-content [content]="parentContext.introductoryText" [hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
|
import {
|
||||||
|
BrowseByMetadataPageComponent,
|
||||||
|
browseParamsToOptions,
|
||||||
|
getBrowseSearchOptions
|
||||||
|
} from './browse-by-metadata-page.component';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -14,7 +18,7 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
@@ -25,6 +29,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
|
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
describe('BrowseByMetadataPageComponent', () => {
|
describe('BrowseByMetadataPageComponent', () => {
|
||||||
let comp: BrowseByMetadataPageComponent;
|
let comp: BrowseByMetadataPageComponent;
|
||||||
@@ -43,6 +48,13 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const environmentMock = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: true,
|
||||||
|
pageSize: 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mockEntries = [
|
const mockEntries = [
|
||||||
{
|
{
|
||||||
type: BrowseEntry.type,
|
type: BrowseEntry.type,
|
||||||
@@ -97,7 +109,8 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
{ provide: BrowseService, useValue: mockBrowseService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: Router, useValue: new RouterMock() }
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
{ provide: APP_CONFIG, useValue: environmentMock }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -118,6 +131,10 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
expect(comp.items$).toBeUndefined();
|
expect(comp.items$).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set embed thumbnail property to true', () => {
|
||||||
|
expect(comp.fetchThumbnails).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
describe('when a value is provided', () => {
|
describe('when a value is provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const paramsWithValue = {
|
const paramsWithValue = {
|
||||||
@@ -145,14 +162,14 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
};
|
};
|
||||||
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
currentPage: 5,
|
currentPage: 5,
|
||||||
pageSize: 10,
|
pageSize: comp.appConfig.browseBy.pageSize,
|
||||||
});
|
});
|
||||||
const sortOptions = {
|
const sortOptions = {
|
||||||
direction: SortDirection.ASC,
|
direction: SortDirection.ASC,
|
||||||
field: 'fake-field',
|
field: 'fake-field',
|
||||||
};
|
};
|
||||||
|
|
||||||
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author');
|
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author', comp.fetchThumbnails);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
||||||
@@ -163,6 +180,36 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
expect(result.sort.direction).toEqual(SortDirection.ASC);
|
expect(result.sort.direction).toEqual(SortDirection.ASC);
|
||||||
expect(result.sort.field).toEqual('fake-field');
|
expect(result.sort.field).toEqual('fake-field');
|
||||||
expect(result.scope).toEqual('fake-scope');
|
expect(result.scope).toEqual('fake-scope');
|
||||||
|
expect(result.fetchThumbnail).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calling getBrowseSearchOptions', () => {
|
||||||
|
let result: BrowseEntrySearchOptions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const paramsScope = {
|
||||||
|
scope: 'fake-scope'
|
||||||
|
};
|
||||||
|
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
currentPage: 5,
|
||||||
|
pageSize: comp.appConfig.browseBy.pageSize,
|
||||||
|
});
|
||||||
|
const sortOptions = {
|
||||||
|
direction: SortDirection.ASC,
|
||||||
|
field: 'fake-field',
|
||||||
|
};
|
||||||
|
|
||||||
|
result = getBrowseSearchOptions('title', paginationOptions, sortOptions, comp.fetchThumbnails);
|
||||||
|
});
|
||||||
|
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
||||||
|
|
||||||
|
expect(result.metadataDefinition).toEqual('title');
|
||||||
|
expect(result.pagination.currentPage).toEqual(5);
|
||||||
|
expect(result.pagination.pageSize).toEqual(10);
|
||||||
|
expect(result.sort.direction).toEqual(SortDirection.ASC);
|
||||||
|
expect(result.sort.field).toEqual('fake-field');
|
||||||
|
expect(result.fetchThumbnail).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -14,9 +14,9 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators';
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
export const BBM_PAGINATION_ID = 'bbm';
|
export const BBM_PAGINATION_ID = 'bbm';
|
||||||
|
|
||||||
@@ -26,12 +26,12 @@ export const BBM_PAGINATION_ID = 'bbm';
|
|||||||
templateUrl: './browse-by-metadata-page.component.html'
|
templateUrl: './browse-by-metadata-page.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component for browsing (items) by metadata definition
|
* Component for browsing (items) by metadata definition.
|
||||||
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
|
* A metadata definition (a.k.a. browse id) is a short term used to describe one
|
||||||
* An example would be 'author' for 'dc.contributor.*'
|
* or multiple metadata fields. An example would be 'author' for
|
||||||
|
* 'dc.contributor.*'
|
||||||
*/
|
*/
|
||||||
@rendersBrowseBy(BrowseByDataType.Metadata)
|
export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||||
export class BrowseByMetadataPageComponent implements OnInit {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of browse-entries to display
|
* The list of browse-entries to display
|
||||||
@@ -51,11 +51,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* The pagination config used to display the values
|
* The pagination config used to display the values
|
||||||
*/
|
*/
|
||||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
paginationConfig: PaginationComponentOptions;
|
||||||
id: BBM_PAGINATION_ID,
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: 20
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pagination observable
|
* The pagination observable
|
||||||
@@ -95,7 +91,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
startsWithOptions;
|
startsWithOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value we're browing items for
|
* The value we're browsing items for
|
||||||
* - When the value is not empty, we're browsing items
|
* - When the value is not empty, we're browsing items
|
||||||
* - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
|
* - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
|
||||||
*/
|
*/
|
||||||
@@ -111,16 +107,31 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
startsWith: string;
|
startsWith: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether to request embedded thumbnail.
|
||||||
|
*/
|
||||||
|
fetchThumbnails: boolean;
|
||||||
|
|
||||||
public constructor(protected route: ActivatedRoute,
|
public constructor(protected route: ActivatedRoute,
|
||||||
protected browseService: BrowseService,
|
protected browseService: BrowseService,
|
||||||
protected dsoService: DSpaceObjectDataService,
|
protected dsoService: DSpaceObjectDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
|
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||||
|
|
||||||
|
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
|
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: BBM_PAGINATION_ID,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: this.appConfig.browseBy.pageSize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -133,15 +144,16 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
this.authority = params.authority;
|
this.authority = params.authority;
|
||||||
this.value = +params.value || params.value || '';
|
this.value = +params.value || params.value || '';
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
|
||||||
if (isNotEmpty(this.value)) {
|
if (isNotEmpty(this.value)) {
|
||||||
this.updatePageWithItems(searchOptions, this.value, this.authority);
|
this.updatePageWithItems(
|
||||||
|
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
||||||
} else {
|
} else {
|
||||||
this.updatePage(searchOptions);
|
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
|
||||||
}
|
}
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
}));
|
}));
|
||||||
this.updateStartsWithTextOptions();
|
this.updateStartsWithTextOptions();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,22 +240,44 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates browse entry search options.
|
||||||
|
* @param defaultBrowseId the metadata definition to fetch entries or items for
|
||||||
|
* @param paginationConfig the required pagination configuration
|
||||||
|
* @param sortConfig the required sort configuration
|
||||||
|
* @param fetchThumbnails optional boolean for fetching thumbnails
|
||||||
|
* @returns BrowseEntrySearchOptions instance
|
||||||
|
*/
|
||||||
|
export function getBrowseSearchOptions(defaultBrowseId: string,
|
||||||
|
paginationConfig: PaginationComponentOptions,
|
||||||
|
sortConfig: SortOptions,
|
||||||
|
fetchThumbnails?: boolean) {
|
||||||
|
if (!hasValue(fetchThumbnails)) {
|
||||||
|
fetchThumbnails = false;
|
||||||
|
}
|
||||||
|
return new BrowseEntrySearchOptions(defaultBrowseId, paginationConfig, sortConfig, null,
|
||||||
|
null, fetchThumbnails);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
|
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
|
||||||
* @param params URL and query parameters
|
* @param params URL and query parameters
|
||||||
* @param paginationConfig Pagination configuration
|
* @param paginationConfig Pagination configuration
|
||||||
* @param sortConfig Sorting configuration
|
* @param sortConfig Sorting configuration
|
||||||
* @param metadata Optional metadata definition to fetch browse entries/items for
|
* @param metadata Optional metadata definition to fetch browse entries/items for
|
||||||
|
* @param fetchThumbnail Optional parameter for requesting thumbnail images
|
||||||
*/
|
*/
|
||||||
export function browseParamsToOptions(params: any,
|
export function browseParamsToOptions(params: any,
|
||||||
paginationConfig: PaginationComponentOptions,
|
paginationConfig: PaginationComponentOptions,
|
||||||
sortConfig: SortOptions,
|
sortConfig: SortOptions,
|
||||||
metadata?: string): BrowseEntrySearchOptions {
|
metadata?: string,
|
||||||
|
fetchThumbnail?: boolean): BrowseEntrySearchOptions {
|
||||||
return new BrowseEntrySearchOptions(
|
return new BrowseEntrySearchOptions(
|
||||||
metadata,
|
metadata,
|
||||||
paginationConfig,
|
paginationConfig,
|
||||||
sortConfig,
|
sortConfig,
|
||||||
+params.startsWith || params.startsWith,
|
+params.startsWith || params.startsWith,
|
||||||
params.scope
|
params.scope,
|
||||||
|
fetchThumbnail
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page.component';
|
||||||
|
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for BrowseByMetadataPageComponent
|
||||||
|
**/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-browse-by-metadata-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
@rendersBrowseBy(BrowseByDataType.Metadata)
|
||||||
|
export class ThemedBrowseByMetadataPageComponent
|
||||||
|
extends ThemedComponent<BrowseByMetadataPageComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'BrowseByMetadataPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./browse-by-metadata-page.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -18,11 +18,11 @@ import { BrowseService } from '../../core/browse/browse.service';
|
|||||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
|
||||||
describe('BrowseByTitlePageComponent', () => {
|
describe('BrowseByTitlePageComponent', () => {
|
||||||
let comp: BrowseByTitlePageComponent;
|
let comp: BrowseByTitlePageComponent;
|
||||||
@@ -77,7 +77,8 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
{ provide: BrowseService, useValue: mockBrowseService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: Router, useValue: new RouterMock() }
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { Component } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import {
|
import {
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
browseParamsToOptions
|
browseParamsToOptions, getBrowseSearchOptions
|
||||||
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
@@ -23,20 +22,21 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
/**
|
/**
|
||||||
* Component for browsing items by title (dc.title)
|
* Component for browsing items by title (dc.title)
|
||||||
*/
|
*/
|
||||||
@rendersBrowseBy(BrowseByDataType.Title)
|
|
||||||
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||||
|
|
||||||
public constructor(protected route: ActivatedRoute,
|
public constructor(protected route: ActivatedRoute,
|
||||||
protected browseService: BrowseService,
|
protected browseService: BrowseService,
|
||||||
protected dsoService: DSpaceObjectDataService,
|
protected dsoService: DSpaceObjectDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(route, browseService, dsoService, paginationService, router);
|
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||||
|
super(route, browseService, dsoService, paginationService, router, appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
// include the thumbnail configuration in browse search options
|
||||||
|
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -47,7 +47,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined);
|
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
}));
|
}));
|
||||||
this.updateStartsWithTextOptions();
|
this.updateStartsWithTextOptions();
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
|
||||||
|
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for BrowseByTitlePageComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-browse-by-title-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
@rendersBrowseBy(BrowseByDataType.Title)
|
||||||
|
export class ThemedBrowseByTitlePageComponent
|
||||||
|
extends ThemedComponent<BrowseByTitlePageComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'BrowseByTitlePageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/browse-by/browse-by-title-page/browse-by-title-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./browse-by-title-page.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -7,12 +7,20 @@ import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-
|
|||||||
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
||||||
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
||||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||||
|
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
||||||
|
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
||||||
|
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
BrowseByDatePageComponent
|
BrowseByDatePageComponent,
|
||||||
|
|
||||||
|
ThemedBrowseByMetadataPageComponent,
|
||||||
|
ThemedBrowseByDatePageComponent,
|
||||||
|
ThemedBrowseByTitlePageComponent,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -17,10 +17,10 @@
|
|||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
|
|
||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-handle
|
<ds-themed-comcol-page-handle
|
||||||
[content]="collection.handle"
|
[content]="collection.handle"
|
||||||
[title]="'collection.page.handle'" >
|
[title]="'collection.page.handle'" >
|
||||||
</ds-comcol-page-handle>
|
</ds-themed-comcol-page-handle>
|
||||||
<!-- Introductory text -->
|
<!-- Introductory text -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.introductoryText"
|
[content]="collection.introductoryText"
|
||||||
|
@@ -28,6 +28,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
|
|||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||||
|
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
@@ -74,6 +75,7 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
this.paginationConfig.pageSize = 5;
|
this.paginationConfig.pageSize = 5;
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -102,13 +104,14 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
map((rd) => rd.payload.id),
|
map((rd) => rd.payload.id),
|
||||||
switchMap((id: string) => {
|
switchMap((id: string) => {
|
||||||
return this.searchService.search(
|
return this.searchService.search<Item>(
|
||||||
new PaginatedSearchOptions({
|
new PaginatedSearchOptions({
|
||||||
scope: id,
|
scope: id,
|
||||||
pagination: currentPagination,
|
pagination: currentPagination,
|
||||||
sort: currentSort,
|
sort: currentSort,
|
||||||
dsoTypes: [DSpaceObjectType.ITEM]
|
dsoTypes: [DSpaceObjectType.ITEM]
|
||||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW)
|
||||||
|
.pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
||||||
}),
|
}),
|
||||||
startWith(undefined) // Make sure switching pages shows loading component
|
startWith(undefined) // Make sure switching pages shows loading component
|
||||||
)
|
)
|
||||||
|
@@ -17,7 +17,7 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
|||||||
followLink('parentCommunity', {},
|
followLink('parentCommunity', {},
|
||||||
followLink('parentCommunity')
|
followLink('parentCommunity')
|
||||||
),
|
),
|
||||||
followLink('logo')
|
followLink('logo'),
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -15,6 +15,8 @@ import { Collection } from '../core/shared/collection.model';
|
|||||||
import { PageInfo } from '../core/shared/page-info.model';
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
import { FlatNode } from './flat-node.model';
|
import { FlatNode } from './flat-node.model';
|
||||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||||
|
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
import { environment } from 'src/environments/environment.test';
|
||||||
|
|
||||||
describe('CommunityListService', () => {
|
describe('CommunityListService', () => {
|
||||||
let store: StoreMock<AppState>;
|
let store: StoreMock<AppState>;
|
||||||
@@ -191,13 +193,14 @@ describe('CommunityListService', () => {
|
|||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [CommunityListService,
|
providers: [CommunityListService,
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
{ provide: Store, useValue: StoreMock },
|
{ provide: Store, useValue: StoreMock },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
store = TestBed.inject(Store as any);
|
store = TestBed.inject(Store as any);
|
||||||
service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store);
|
service = new CommunityListService(environment, communityDataServiceStub, collectionDataServiceStub, store);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {
|
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { createSelector, Store } from '@ngrx/store';
|
import { createSelector, Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
@@ -23,6 +23,7 @@ import { followLink } from '../shared/utils/follow-link-config.model';
|
|||||||
import { FlatNode } from './flat-node.model';
|
import { FlatNode } from './flat-node.model';
|
||||||
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||||
|
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
|
||||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||||
@@ -80,8 +81,6 @@ const communityListStateSelector = (state: AppState) => state.communityList;
|
|||||||
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
||||||
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
||||||
|
|
||||||
export const MAX_COMCOLS_PER_PAGE = 20;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
||||||
* and connection to the store to retrieve and save the state of the community list
|
* and connection to the store to retrieve and save the state of the community list
|
||||||
@@ -89,8 +88,15 @@ export const MAX_COMCOLS_PER_PAGE = 20;
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityListService {
|
export class CommunityListService {
|
||||||
|
|
||||||
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
private pageSize: number;
|
||||||
private store: Store<any>) {
|
|
||||||
|
constructor(
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
private communityDataService: CommunityDataService,
|
||||||
|
private collectionDataService: CollectionDataService,
|
||||||
|
private store: Store<any>
|
||||||
|
) {
|
||||||
|
this.pageSize = appConfig.communityList.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
|
private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
@@ -145,7 +151,7 @@ export class CommunityListService {
|
|||||||
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
|
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
|
||||||
return this.communityDataService.findTop({
|
return this.communityDataService.findTop({
|
||||||
currentPage: options.currentPage,
|
currentPage: options.currentPage,
|
||||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
elementsPerPage: this.pageSize,
|
||||||
sort: {
|
sort: {
|
||||||
field: options.sort.field,
|
field: options.sort.field,
|
||||||
direction: options.sort.direction
|
direction: options.sort.direction
|
||||||
@@ -216,7 +222,7 @@ export class CommunityListService {
|
|||||||
let subcoms = [];
|
let subcoms = [];
|
||||||
for (let i = 1; i <= currentCommunityPage; i++) {
|
for (let i = 1; i <= currentCommunityPage; i++) {
|
||||||
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
elementsPerPage: this.pageSize,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
},
|
},
|
||||||
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
||||||
@@ -241,7 +247,7 @@ export class CommunityListService {
|
|||||||
let collections = [];
|
let collections = [];
|
||||||
for (let i = 1; i <= currentCollectionPage; i++) {
|
for (let i = 1; i <= currentCollectionPage; i++) {
|
||||||
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
elementsPerPage: this.pageSize,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@@ -10,8 +10,8 @@
|
|||||||
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
|
<ds-themed-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
|
||||||
</ds-comcol-page-handle>
|
</ds-themed-comcol-page-handle>
|
||||||
<!-- Introductory text -->
|
<!-- Introductory text -->
|
||||||
<ds-comcol-page-content [content]="communityPayload.introductoryText" [hasInnerHtml]="true">
|
<ds-comcol-page-content [content]="communityPayload.introductoryText" [hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
@@ -25,12 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||||
</ds-themed-comcol-page-browse-by>
|
</ds-themed-comcol-page-browse-by>
|
||||||
|
|
||||||
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
<ds-themed-community-page-sub-community-list [community]="communityPayload"></ds-themed-community-page-sub-community-list>
|
||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-themed-community-page-sub-collection-list [community]="communityPayload"></ds-themed-community-page-sub-collection-list>
|
||||||
</section>
|
</section>
|
||||||
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
|
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
|
@@ -13,10 +13,18 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
import { CommunityFormModule } from './community-form/community-form.module';
|
import { CommunityFormModule } from './community-form/community-form.module';
|
||||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||||
|
import {
|
||||||
|
ThemedCommunityPageSubCommunityListComponent
|
||||||
|
} from './sub-community-list/themed-community-page-sub-community-list.component';
|
||||||
|
import {
|
||||||
|
ThemedCollectionPageSubCollectionListComponent
|
||||||
|
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
|
||||||
|
|
||||||
const DECLARATIONS = [CommunityPageComponent,
|
const DECLARATIONS = [CommunityPageComponent,
|
||||||
ThemedCommunityPageComponent,
|
ThemedCommunityPageComponent,
|
||||||
|
ThemedCommunityPageSubCommunityListComponent,
|
||||||
CommunityPageSubCollectionListComponent,
|
CommunityPageSubCollectionListComponent,
|
||||||
|
ThemedCollectionPageSubCollectionListComponent,
|
||||||
CommunityPageSubCommunityListComponent,
|
CommunityPageSubCommunityListComponent,
|
||||||
CreateCommunityPageComponent,
|
CreateCommunityPageComponent,
|
||||||
DeleteCommunityPageComponent];
|
DeleteCommunityPageComponent];
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-comcol-role
|
<ds-comcol-role
|
||||||
*ngFor="let comcolRole of getComcolRoles$() | async"
|
*ngFor="let comcolRole of comcolRoles$ | async"
|
||||||
[dso]="community$ | async"
|
[dso]="community$ | async"
|
||||||
[comcolRole]="comcolRole"
|
[comcolRole]="comcolRole"
|
||||||
>
|
>
|
||||||
|
@@ -78,8 +78,9 @@ describe('CommunityRolesComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a community admin role component', () => {
|
it('should display a community admin role component', (done) => {
|
||||||
expect(de.query(By.css('ds-comcol-role .community-admin')))
|
expect(de.query(By.css('ds-comcol-role .community-admin')))
|
||||||
.toBeTruthy();
|
.toBeTruthy();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,28 +19,14 @@ export class CommunityRolesComponent implements OnInit {
|
|||||||
dsoRD$: Observable<RemoteData<Community>>;
|
dsoRD$: Observable<RemoteData<Community>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The community to manage, as an observable.
|
* The different roles for the community, as an observable.
|
||||||
*/
|
*/
|
||||||
get community$(): Observable<Community> {
|
comcolRoles$: Observable<HALLink[]>;
|
||||||
return this.dsoRD$.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The different roles for the community.
|
* The community to manage, as an observable.
|
||||||
*/
|
*/
|
||||||
getComcolRoles$(): Observable<HALLink[]> {
|
community$: Observable<Community>;
|
||||||
return this.community$.pipe(
|
|
||||||
map((community) => [
|
|
||||||
{
|
|
||||||
name: 'community-admin',
|
|
||||||
href: community._links.adminGroup.href,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
@@ -52,5 +38,22 @@ export class CommunityRolesComponent implements OnInit {
|
|||||||
first(),
|
first(),
|
||||||
map((data) => data.dso),
|
map((data) => data.dso),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.community$ = this.dsoRD$.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different roles for the community.
|
||||||
|
*/
|
||||||
|
this.comcolRoles$ = this.community$.pipe(
|
||||||
|
map((community) => [
|
||||||
|
{
|
||||||
|
name: 'community-admin',
|
||||||
|
href: community._links.adminGroup.href,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -180,7 +180,9 @@ describe('CommunityPageSubCollectionList Component', () => {
|
|||||||
comp.community = mockCommunity;
|
comp.community = mockCommunity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should display a list of collections', () => {
|
it('should display a list of collections', () => {
|
||||||
|
waitForAsync(() => {
|
||||||
subCollList = collections;
|
subCollList = collections;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -192,6 +194,7 @@ describe('CommunityPageSubCollectionList Component', () => {
|
|||||||
expect(collList[3].nativeElement.textContent).toContain('Collection 4');
|
expect(collList[3].nativeElement.textContent).toContain('Collection 4');
|
||||||
expect(collList[4].nativeElement.textContent).toContain('Collection 5');
|
expect(collList[4].nativeElement.textContent).toContain('Collection 5');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not display the header when list of collections is empty', () => {
|
it('should not display the header when list of collections is empty', () => {
|
||||||
subCollList = [];
|
subCollList = [];
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
|
|||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page-sub-collection-list',
|
selector: 'ds-community-page-sub-collection-list',
|
||||||
@@ -19,9 +20,15 @@ import { switchMap } from 'rxjs/operators';
|
|||||||
templateUrl: './community-page-sub-collection-list.component.html',
|
templateUrl: './community-page-sub-collection-list.component.html',
|
||||||
animations:[fadeIn]
|
animations:[fadeIn]
|
||||||
})
|
})
|
||||||
export class CommunityPageSubCollectionListComponent implements OnInit {
|
export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy {
|
||||||
@Input() community: Community;
|
@Input() community: Community;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional page size. Overrides communityList.pageSize configuration for this component.
|
||||||
|
* Value can be added in the themed version of the parent component.
|
||||||
|
*/
|
||||||
|
@Input() pageSize: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pagination configuration
|
* The pagination configuration
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +57,9 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.config = new PaginationComponentOptions();
|
this.config = new PaginationComponentOptions();
|
||||||
this.config.id = this.pageId;
|
this.config.id = this.pageId;
|
||||||
this.config.pageSize = 5;
|
if (hasValue(this.pageSize)) {
|
||||||
|
this.config.pageSize = this.pageSize;
|
||||||
|
}
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.initPage();
|
this.initPage();
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-community-page-sub-collection-list',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent<CommunityPageSubCollectionListComponent> {
|
||||||
|
@Input() community: Community;
|
||||||
|
@Input() pageSize: number;
|
||||||
|
protected inAndOutputNames: (keyof CommunityPageSubCollectionListComponent & keyof this)[] = ['community', 'pageSize'];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'CommunityPageSubCollectionListComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./community-page-sub-collection-list.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -181,7 +181,9 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should display a list of sub-communities', () => {
|
it('should display a list of sub-communities', () => {
|
||||||
|
waitForAsync(() => {
|
||||||
subCommList = subcommunities;
|
subCommList = subcommunities;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -193,6 +195,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
|||||||
expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4');
|
expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4');
|
||||||
expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5');
|
expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not display the header when list of sub-communities is empty', () => {
|
it('should not display the header when list of sub-communities is empty', () => {
|
||||||
subCommList = [];
|
subCommList = [];
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
|
import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page-sub-community-list',
|
selector: 'ds-community-page-sub-community-list',
|
||||||
@@ -22,9 +23,15 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
|||||||
/**
|
/**
|
||||||
* Component to render the sub-communities of a Community
|
* Component to render the sub-communities of a Community
|
||||||
*/
|
*/
|
||||||
export class CommunityPageSubCommunityListComponent implements OnInit {
|
export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy {
|
||||||
@Input() community: Community;
|
@Input() community: Community;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional page size. Overrides communityList.pageSize configuration for this component.
|
||||||
|
* Value can be added in the themed version of the parent component.
|
||||||
|
*/
|
||||||
|
@Input() pageSize: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pagination configuration
|
* The pagination configuration
|
||||||
*/
|
*/
|
||||||
@@ -53,7 +60,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.config = new PaginationComponentOptions();
|
this.config = new PaginationComponentOptions();
|
||||||
this.config.id = this.pageId;
|
this.config.id = this.pageId;
|
||||||
this.config.pageSize = 5;
|
if (hasValue(this.pageSize)) {
|
||||||
|
this.config.pageSize = this.pageSize;
|
||||||
|
}
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.initPage();
|
this.initPage();
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-community-page-sub-community-list',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent<CommunityPageSubCommunityListComponent> {
|
||||||
|
|
||||||
|
@Input() community: Community;
|
||||||
|
@Input() pageSize: number;
|
||||||
|
protected inAndOutputNames: (keyof CommunityPageSubCommunityListComponent & keyof this)[] = ['community', 'pageSize'];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'CommunityPageSubCommunityListComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./community-page-sub-community-list.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -111,7 +111,6 @@ describe(`AuthRequestService`, () => {
|
|||||||
body: undefined,
|
body: undefined,
|
||||||
options,
|
options,
|
||||||
}));
|
}));
|
||||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -151,7 +150,6 @@ describe(`AuthRequestService`, () => {
|
|||||||
body: { content: 'something' },
|
body: { content: 'something' },
|
||||||
options,
|
options,
|
||||||
}));
|
}));
|
||||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -58,7 +58,9 @@ export abstract class AuthRequestService {
|
|||||||
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
|
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
this.halService.getEndpoint(this.linkName).pipe(
|
const endpoint$ = this.halService.getEndpoint(this.linkName);
|
||||||
|
|
||||||
|
endpoint$.pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@@ -68,7 +70,9 @@ export abstract class AuthRequestService {
|
|||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.fetchRequest(requestId);
|
return endpoint$.pipe(
|
||||||
|
switchMap(() => this.fetchRequest(requestId)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +83,9 @@ export abstract class AuthRequestService {
|
|||||||
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
|
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
this.halService.getEndpoint(this.linkName).pipe(
|
const endpoint$ = this.halService.getEndpoint(this.linkName);
|
||||||
|
|
||||||
|
endpoint$.pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
|
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@@ -89,7 +95,9 @@ export abstract class AuthRequestService {
|
|||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.fetchRequest(requestId, ...linksToFollow);
|
return endpoint$.pipe(
|
||||||
|
switchMap(() => this.fetchRequest(requestId, ...linksToFollow)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||||
|
@@ -31,6 +31,8 @@ export class DSONameService {
|
|||||||
const givenName = dso.firstMetadataValue('person.givenName');
|
const givenName = dso.firstMetadataValue('person.givenName');
|
||||||
if (isEmpty(familyName) && isEmpty(givenName)) {
|
if (isEmpty(familyName) && isEmpty(givenName)) {
|
||||||
return dso.firstMetadataValue('dc.title') || dso.name;
|
return dso.firstMetadataValue('dc.title') || dso.name;
|
||||||
|
} else if (isEmpty(familyName) || isEmpty(givenName)) {
|
||||||
|
return familyName || givenName;
|
||||||
} else {
|
} else {
|
||||||
return `${familyName}, ${givenName}`;
|
return `${familyName}, ${givenName}`;
|
||||||
}
|
}
|
||||||
@@ -55,11 +57,14 @@ export class DSONameService {
|
|||||||
.filter((type) => typeof type === 'string')
|
.filter((type) => typeof type === 'string')
|
||||||
.find((type: string) => Object.keys(this.factories).includes(type)) as string;
|
.find((type: string) => Object.keys(this.factories).includes(type)) as string;
|
||||||
|
|
||||||
|
let name;
|
||||||
if (hasValue(match)) {
|
if (hasValue(match)) {
|
||||||
return this.factories[match](dso);
|
name = this.factories[match](dso);
|
||||||
} else {
|
|
||||||
return this.factories.Default(dso);
|
|
||||||
}
|
}
|
||||||
|
if (isEmpty(name)) {
|
||||||
|
name = this.factories.Default(dso);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,16 @@ import { SortOptions } from '../cache/models/sort-options.model';
|
|||||||
* - metadataDefinition: The metadata definition to fetch entries or items for
|
* - metadataDefinition: The metadata definition to fetch entries or items for
|
||||||
* - pagination: Optional pagination options to use
|
* - pagination: Optional pagination options to use
|
||||||
* - sort: Optional sorting options to use
|
* - sort: Optional sorting options to use
|
||||||
|
* - startsWith An optional value to use to filter the browse results
|
||||||
* - scope: An optional scope to limit the results within a specific collection or community
|
* - scope: An optional scope to limit the results within a specific collection or community
|
||||||
|
* - fetchThumbnail An optional boolean to request thumbnail for items
|
||||||
*/
|
*/
|
||||||
export class BrowseEntrySearchOptions {
|
export class BrowseEntrySearchOptions {
|
||||||
constructor(public metadataDefinition: string,
|
constructor(public metadataDefinition: string,
|
||||||
public pagination?: PaginationComponentOptions,
|
public pagination?: PaginationComponentOptions,
|
||||||
public sort?: SortOptions,
|
public sort?: SortOptions,
|
||||||
public startsWith?: string,
|
public startsWith?: string,
|
||||||
public scope?: string) {
|
public scope?: string,
|
||||||
|
public fetchThumbnail?: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,12 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
||||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
|
||||||
|
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
||||||
|
followLink('thumbnail')
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all browse requests
|
* The service handling all browse requests
|
||||||
@@ -96,6 +102,9 @@ export class BrowseService {
|
|||||||
return href;
|
return href;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (options.fetchThumbnail ) {
|
||||||
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||||
|
}
|
||||||
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +150,9 @@ export class BrowseService {
|
|||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
if (options.fetchThumbnail) {
|
||||||
|
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||||
|
}
|
||||||
return this.hrefOnlyDataService.findListByHref<Item>(href$);
|
return this.hrefOnlyDataService.findListByHref<Item>(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
|
||||||
import { Item } from '../../shared/item.model';
|
import { Item } from '../../shared/item.model';
|
||||||
import { PageInfo } from '../../shared/page-info.model';
|
import { PageInfo } from '../../shared/page-info.model';
|
||||||
@@ -18,6 +18,9 @@ import { take } from 'rxjs/operators';
|
|||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { RequestEntryState } from '../../data/request-entry-state.model';
|
import { RequestEntryState } from '../../data/request-entry-state.model';
|
||||||
import { RequestEntry } from '../../data/request-entry.model';
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
describe('RemoteDataBuildService', () => {
|
describe('RemoteDataBuildService', () => {
|
||||||
let service: RemoteDataBuildService;
|
let service: RemoteDataBuildService;
|
||||||
@@ -646,4 +649,211 @@ describe('RemoteDataBuildService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildFromHref', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(objectCache.getRequestUUIDBySelfLink as jasmine.Spy).and.returnValue(cold('a', { a: 'request/uuid' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID emit nothing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit anything', () => {
|
||||||
|
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold(''));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID emits nothing', () => {
|
||||||
|
let requestEntry: RequestEntry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestEntry = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.Success,
|
||||||
|
request: {},
|
||||||
|
});
|
||||||
|
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry }));
|
||||||
|
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create remote-data with the existing request-entry', () => {
|
||||||
|
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
|
||||||
|
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID is stale', () => {
|
||||||
|
let requestEntry1: RequestEntry;
|
||||||
|
let requestEntry2: RequestEntry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestEntry1 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.Success,
|
||||||
|
request: {},
|
||||||
|
});
|
||||||
|
requestEntry2 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.SuccessStale,
|
||||||
|
request: {},
|
||||||
|
});
|
||||||
|
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
|
||||||
|
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create remote-data with the non-stale request-entry', () => {
|
||||||
|
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
|
||||||
|
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are stale', () => {
|
||||||
|
let requestEntry1: RequestEntry;
|
||||||
|
let requestEntry2: RequestEntry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestEntry1 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.SuccessStale,
|
||||||
|
request: {},
|
||||||
|
lastUpdated: 20,
|
||||||
|
});
|
||||||
|
requestEntry2 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.SuccessStale,
|
||||||
|
request: {},
|
||||||
|
lastUpdated: 10,
|
||||||
|
});
|
||||||
|
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
|
||||||
|
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create remote-data with the most up-to-date request-entry', () => {
|
||||||
|
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
|
||||||
|
a: new RemoteData(undefined, undefined, 20, RequestEntryState.SuccessStale, undefined, {}, undefined),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are not stale', () => {
|
||||||
|
let requestEntry1: RequestEntry;
|
||||||
|
let requestEntry2: RequestEntry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestEntry1 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.Success,
|
||||||
|
request: {},
|
||||||
|
lastUpdated: 25,
|
||||||
|
});
|
||||||
|
requestEntry2 = Object.assign(new RequestEntry(), {
|
||||||
|
state: RequestEntryState.Success,
|
||||||
|
request: {},
|
||||||
|
lastUpdated: 5,
|
||||||
|
});
|
||||||
|
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
|
||||||
|
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create remote-data with the most up-to-date request-entry', () => {
|
||||||
|
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
|
||||||
|
a: new RemoteData(undefined, undefined, 25, RequestEntryState.Success, undefined, {}, undefined),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildFromRequestUUIDAndAwait', () => {
|
||||||
|
let testScheduler;
|
||||||
|
|
||||||
|
let callback: jasmine.Spy;
|
||||||
|
let buildFromRequestUUIDSpy;
|
||||||
|
|
||||||
|
const BOOLEAN = { t: true, f: false };
|
||||||
|
|
||||||
|
const MOCK_PENDING_RD = createPendingRemoteDataObject();
|
||||||
|
const MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
|
||||||
|
const MOCK_FAILED_RD = createFailedRemoteDataObject('failed');
|
||||||
|
|
||||||
|
const RDs = {
|
||||||
|
p: MOCK_PENDING_RD,
|
||||||
|
s: MOCK_SUCCEEDED_RD,
|
||||||
|
f: MOCK_FAILED_RD,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
callback = jasmine.createSpy('callback');
|
||||||
|
callback.and.returnValue(observableOf(undefined));
|
||||||
|
buildFromRequestUUIDSpy = spyOn(service, 'buildFromRequestUUID').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should patch through href & followLinks to buildFromRequestUUID', () => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
service.buildFromRequestUUIDAndAwait('some-href', callback, ...linksToFollow);
|
||||||
|
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith('some-href', ...linksToFollow);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger the callback on successful RD', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
|
||||||
|
expect(rd).toBe(MOCK_SUCCEEDED_RD);
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger the callback on successful RD even if nothing subscribes to the returned Observable', fakeAsync(() => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.buildFromRequestUUIDAndAwait('some-href', callback);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not trigger the callback on pending RD', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_PENDING_RD));
|
||||||
|
|
||||||
|
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
|
||||||
|
expect(rd).toBe(MOCK_PENDING_RD);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not trigger the callback on failed RD', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
|
||||||
|
|
||||||
|
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
|
||||||
|
expect(rd).toBe(MOCK_FAILED_RD);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only emit after the callback is done', () => {
|
||||||
|
testScheduler.run(({ cold: tsCold, expectObservable }) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(
|
||||||
|
tsCold('-p----s', RDs)
|
||||||
|
);
|
||||||
|
callback.and.returnValue(
|
||||||
|
tsCold(' --t', BOOLEAN)
|
||||||
|
);
|
||||||
|
|
||||||
|
const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback);
|
||||||
|
expectObservable(done$).toBe(
|
||||||
|
' -p------s', RDs // resulting duration between pending & successful includes the callback
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
AsyncSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
race as observableRace
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map, switchMap, filter, distinctUntilKeyChanged } from 'rxjs/operators';
|
import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators';
|
||||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -21,10 +21,11 @@ import { HALResource } from '../../shared/hal-resource.model';
|
|||||||
import { PAGINATED_LIST } from '../../data/paginated-list.resource-type';
|
import { PAGINATED_LIST } from '../../data/paginated-list.resource-type';
|
||||||
import { getUrlWithoutEmbedParams } from '../../index/index.selectors';
|
import { getUrlWithoutEmbedParams } from '../../index/index.selectors';
|
||||||
import { getResourceTypeValueFor } from '../object-cache.reducer';
|
import { getResourceTypeValueFor } from '../object-cache.reducer';
|
||||||
import { hasSucceeded, RequestEntryState } from '../../data/request-entry-state.model';
|
import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-entry-state.model';
|
||||||
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
|
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
|
||||||
import { RequestEntry } from '../../data/request-entry.model';
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
import { ResponseState } from '../../data/response-state.model';
|
import { ResponseState } from '../../data/response-state.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
@@ -189,6 +190,49 @@ export class RemoteDataBuildService {
|
|||||||
return this.toRemoteDataObservable<T>(requestEntry$, payload$);
|
return this.toRemoteDataObservable<T>(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link RemoteData} object for a rest request and its response
|
||||||
|
* and emits it only after the callback function is completed.
|
||||||
|
*
|
||||||
|
* @param requestUUID$ The UUID of the request we want to retrieve
|
||||||
|
* @param callback A function that returns an Observable. It will only be called once the request has succeeded.
|
||||||
|
* Then, the response will only be emitted after this callback function has emitted.
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
buildFromRequestUUIDAndAwait<T>(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<T>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<T>> {
|
||||||
|
const response$ = this.buildFromRequestUUID(requestUUID$, ...linksToFollow);
|
||||||
|
|
||||||
|
const callbackDone$ = new AsyncSubject<boolean>();
|
||||||
|
response$.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<any>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
// if the request succeeded, execute the callback
|
||||||
|
return callback(rd);
|
||||||
|
} else {
|
||||||
|
// otherwise, emit right away so the subscription doesn't stick around
|
||||||
|
return [true];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).subscribe(() => {
|
||||||
|
callbackDone$.next(true);
|
||||||
|
callbackDone$.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
return response$.pipe(
|
||||||
|
switchMap((rd: RemoteData<any>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
// if the request succeeded, wait for the callback to finish
|
||||||
|
return callbackDone$.pipe(
|
||||||
|
map(() => rd),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [rd];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link RemoteData} object for a rest request and its response
|
* Creates a {@link RemoteData} object for a rest request and its response
|
||||||
*
|
*
|
||||||
@@ -207,10 +251,27 @@ export class RemoteDataBuildService {
|
|||||||
this.objectCache.getRequestUUIDBySelfLink(href)),
|
this.objectCache.getRequestUUIDBySelfLink(href)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntry$ = observableRace(
|
const requestEntry$ = observableCombineLatest([
|
||||||
href$.pipe(getRequestFromRequestHref(this.requestService)),
|
href$.pipe(getRequestFromRequestHref(this.requestService), startWith(undefined)),
|
||||||
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
|
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService), startWith(undefined)),
|
||||||
).pipe(
|
]).pipe(
|
||||||
|
filter(([r1, r2]) => hasValue(r1) || hasValue(r2)),
|
||||||
|
map(([r1, r2]) => {
|
||||||
|
// If one of the two requests has no value, return the other (both is impossible due to the filter above)
|
||||||
|
if (hasNoValue(r2)) {
|
||||||
|
return r1;
|
||||||
|
} else if (hasNoValue(r1)) {
|
||||||
|
return r2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((isStale(r1.state) && isStale(r2.state)) || (!isStale(r1.state) && !isStale(r2.state))) {
|
||||||
|
// Neither or both are stale, pick the most recent request
|
||||||
|
return r1.lastUpdated >= r2.lastUpdated ? r1 : r2;
|
||||||
|
} else {
|
||||||
|
// One of the two is stale, return the not stale request
|
||||||
|
return isStale(r2.state) ? r1 : r2;
|
||||||
|
}
|
||||||
|
}),
|
||||||
distinctUntilKeyChanged('lastUpdated')
|
distinctUntilKeyChanged('lastUpdated')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
48
src/app/core/cache/object-cache.actions.ts
vendored
48
src/app/core/cache/object-cache.actions.ts
vendored
@@ -13,7 +13,9 @@ export const ObjectCacheActionTypes = {
|
|||||||
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
|
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
|
||||||
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
|
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
|
||||||
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
|
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH'),
|
||||||
|
ADD_DEPENDENTS: type('dspace/core/cache/object/ADD_DEPENDENTS'),
|
||||||
|
REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS')
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,6 +128,46 @@ export class ApplyPatchObjectCacheAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An NgRx action to add dependent request UUIDs to a cached object
|
||||||
|
*/
|
||||||
|
export class AddDependentsObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.ADD_DEPENDENTS;
|
||||||
|
payload: {
|
||||||
|
href: string;
|
||||||
|
dependentRequestUUIDs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddDependentsObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href the self link of a cached object
|
||||||
|
* @param dependentRequestUUIDs the UUID of the request that depends on this object
|
||||||
|
*/
|
||||||
|
constructor(href: string, dependentRequestUUIDs: string[]) {
|
||||||
|
this.payload = {
|
||||||
|
href,
|
||||||
|
dependentRequestUUIDs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An NgRx action to remove all dependent request UUIDs from a cached object
|
||||||
|
*/
|
||||||
|
export class RemoveDependentsObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.REMOVE_DEPENDENTS;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RemoveDependentsObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href the self link of a cached object for which to remove all dependent request UUIDs
|
||||||
|
*/
|
||||||
|
constructor(href: string) {
|
||||||
|
this.payload = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type to encompass all ObjectCacheActions
|
* A type to encompass all ObjectCacheActions
|
||||||
@@ -135,4 +177,6 @@ export type ObjectCacheAction
|
|||||||
| RemoveFromObjectCacheAction
|
| RemoveFromObjectCacheAction
|
||||||
| ResetObjectCacheTimestampsAction
|
| ResetObjectCacheTimestampsAction
|
||||||
| AddPatchObjectCacheAction
|
| AddPatchObjectCacheAction
|
||||||
| ApplyPatchObjectCacheAction;
|
| ApplyPatchObjectCacheAction
|
||||||
|
| AddDependentsObjectCacheAction
|
||||||
|
| RemoveDependentsObjectCacheAction;
|
||||||
|
28
src/app/core/cache/object-cache.reducer.spec.ts
vendored
28
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -2,11 +2,13 @@ import * as deepFreeze from 'deep-freeze';
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import {
|
import {
|
||||||
|
AddDependentsObjectCacheAction,
|
||||||
AddPatchObjectCacheAction,
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
ApplyPatchObjectCacheAction,
|
ApplyPatchObjectCacheAction,
|
||||||
|
RemoveDependentsObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction,
|
RemoveFromObjectCacheAction,
|
||||||
ResetObjectCacheTimestampsAction
|
ResetObjectCacheTimestampsAction,
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
|
|
||||||
import { objectCacheReducer } from './object-cache.reducer';
|
import { objectCacheReducer } from './object-cache.reducer';
|
||||||
@@ -42,20 +44,22 @@ describe('objectCacheReducer', () => {
|
|||||||
timeCompleted: new Date().getTime(),
|
timeCompleted: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestUUIDs: [requestUUID1],
|
requestUUIDs: [requestUUID1],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
patches: [],
|
patches: [],
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
},
|
},
|
||||||
[selfLink2]: {
|
[selfLink2]: {
|
||||||
data: {
|
data: {
|
||||||
type: Item.type,
|
type: Item.type,
|
||||||
self: requestUUID2,
|
self: selfLink2,
|
||||||
foo: 'baz',
|
foo: 'baz',
|
||||||
_links: { self: { href: requestUUID2 } }
|
_links: { self: { href: selfLink2 } }
|
||||||
},
|
},
|
||||||
alternativeLinks: [altLink3, altLink4],
|
alternativeLinks: [altLink3, altLink4],
|
||||||
timeCompleted: new Date().getTime(),
|
timeCompleted: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestUUIDs: [selfLink2],
|
requestUUIDs: [requestUUID2],
|
||||||
|
dependentRequestUUIDs: [requestUUID1],
|
||||||
patches: [],
|
patches: [],
|
||||||
isDirty: false
|
isDirty: false
|
||||||
}
|
}
|
||||||
@@ -189,4 +193,20 @@ describe('objectCacheReducer', () => {
|
|||||||
expect((newState[selfLink1].data as any).name).toEqual(newName);
|
expect((newState[selfLink1].data as any).name).toEqual(newName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add dependent requests on ADD_DEPENDENTS', () => {
|
||||||
|
let newState = objectCacheReducer(testState, new AddDependentsObjectCacheAction(selfLink1, ['new', 'newer', 'newest']));
|
||||||
|
expect(newState[selfLink1].dependentRequestUUIDs).toEqual(['new', 'newer', 'newest']);
|
||||||
|
|
||||||
|
newState = objectCacheReducer(newState, new AddDependentsObjectCacheAction(selfLink2, ['more']));
|
||||||
|
expect(newState[selfLink2].dependentRequestUUIDs).toEqual([requestUUID1, 'more']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear dependent requests on REMOVE_DEPENDENTS', () => {
|
||||||
|
let newState = objectCacheReducer(testState, new RemoveDependentsObjectCacheAction(selfLink1));
|
||||||
|
expect(newState[selfLink1].dependentRequestUUIDs).toEqual([]);
|
||||||
|
|
||||||
|
newState = objectCacheReducer(newState, new RemoveDependentsObjectCacheAction(selfLink2));
|
||||||
|
expect(newState[selfLink2].dependentRequestUUIDs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
66
src/app/core/cache/object-cache.reducer.ts
vendored
66
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,12 +1,13 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import {
|
import {
|
||||||
|
AddDependentsObjectCacheAction,
|
||||||
AddPatchObjectCacheAction,
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
ApplyPatchObjectCacheAction,
|
ApplyPatchObjectCacheAction,
|
||||||
ObjectCacheAction,
|
ObjectCacheAction,
|
||||||
ObjectCacheActionTypes,
|
ObjectCacheActionTypes, RemoveDependentsObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction,
|
RemoveFromObjectCacheAction,
|
||||||
ResetObjectCacheTimestampsAction
|
ResetObjectCacheTimestampsAction,
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CacheEntry } from './cache-entry';
|
import { CacheEntry } from './cache-entry';
|
||||||
@@ -69,6 +70,12 @@ export class ObjectCacheEntry implements CacheEntry {
|
|||||||
*/
|
*/
|
||||||
requestUUIDs: string[];
|
requestUUIDs: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of UUIDs for the requests that depend on this object.
|
||||||
|
* When this object is invalidated, these requests will be invalidated as well.
|
||||||
|
*/
|
||||||
|
dependentRequestUUIDs: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of patches that were made on the client side to this entry, but haven't been sent to the server yet
|
* An array of patches that were made on the client side to this entry, but haven't been sent to the server yet
|
||||||
*/
|
*/
|
||||||
@@ -134,6 +141,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
|||||||
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
|
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.ADD_DEPENDENTS: {
|
||||||
|
return addDependentsObjectCacheState(state, action as AddDependentsObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.REMOVE_DEPENDENTS: {
|
||||||
|
return removeDependentsObjectCacheState(state, action as RemoveDependentsObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -159,6 +174,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio
|
|||||||
timeCompleted: action.payload.timeCompleted,
|
timeCompleted: action.payload.timeCompleted,
|
||||||
msToLive: action.payload.msToLive,
|
msToLive: action.payload.msToLive,
|
||||||
requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])],
|
requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])],
|
||||||
|
dependentRequestUUIDs: existing.dependentRequestUUIDs || [],
|
||||||
isDirty: isNotEmpty(existing.patches),
|
isDirty: isNotEmpty(existing.patches),
|
||||||
patches: existing.patches || [],
|
patches: existing.patches || [],
|
||||||
alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks]
|
alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks]
|
||||||
@@ -252,3 +268,49 @@ function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObject
|
|||||||
}
|
}
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of dependent request UUIDs to a cached object, used when defining new dependencies
|
||||||
|
*
|
||||||
|
* @param state the current state
|
||||||
|
* @param action an AddDependentsObjectCacheAction
|
||||||
|
* @return the new state, with the dependent requests of the cached object updated
|
||||||
|
*/
|
||||||
|
function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDependentsObjectCacheAction): ObjectCacheState {
|
||||||
|
const href = action.payload.href;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
|
||||||
|
if (hasValue(newState[href])) {
|
||||||
|
newState[href] = Object.assign({}, newState[href], {
|
||||||
|
dependentRequestUUIDs: [
|
||||||
|
...new Set([
|
||||||
|
...newState[href]?.dependentRequestUUIDs || [],
|
||||||
|
...action.payload.dependentRequestUUIDs,
|
||||||
|
])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all dependent request UUIDs from a cached object, used to clear out-of-date depedencies
|
||||||
|
*
|
||||||
|
* @param state the current state
|
||||||
|
* @param action an AddDependentsObjectCacheAction
|
||||||
|
* @return the new state, with the dependent requests of the cached object updated
|
||||||
|
*/
|
||||||
|
function removeDependentsObjectCacheState(state: ObjectCacheState, action: RemoveDependentsObjectCacheAction): ObjectCacheState {
|
||||||
|
const href = action.payload;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
|
||||||
|
if (hasValue(newState[href])) {
|
||||||
|
newState[href] = Object.assign({}, newState[href], {
|
||||||
|
dependentRequestUUIDs: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
133
src/app/core/cache/object-cache.service.spec.ts
vendored
133
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -11,10 +11,12 @@ import { coreReducers} from '../core.reducers';
|
|||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import {
|
import {
|
||||||
|
AddDependentsObjectCacheAction,
|
||||||
|
RemoveDependentsObjectCacheAction,
|
||||||
AddPatchObjectCacheAction,
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
ApplyPatchObjectCacheAction,
|
ApplyPatchObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction
|
RemoveFromObjectCacheAction,
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { Patch } from './object-cache.reducer';
|
import { Patch } from './object-cache.reducer';
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
@@ -25,6 +27,7 @@ import { storeModuleConfig } from '../../app.reducer';
|
|||||||
import { TestColdObservable } from 'jasmine-marbles/src/test-observables';
|
import { TestColdObservable } from 'jasmine-marbles/src/test-observables';
|
||||||
import { IndexName } from '../index/index-name.model';
|
import { IndexName } from '../index/index-name.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
describe('ObjectCacheService', () => {
|
describe('ObjectCacheService', () => {
|
||||||
let service: ObjectCacheService;
|
let service: ObjectCacheService;
|
||||||
@@ -38,6 +41,7 @@ describe('ObjectCacheService', () => {
|
|||||||
let altLink1;
|
let altLink1;
|
||||||
let altLink2;
|
let altLink2;
|
||||||
let requestUUID;
|
let requestUUID;
|
||||||
|
let requestUUID2;
|
||||||
let alternativeLink;
|
let alternativeLink;
|
||||||
let timestamp;
|
let timestamp;
|
||||||
let timestamp2;
|
let timestamp2;
|
||||||
@@ -55,6 +59,7 @@ describe('ObjectCacheService', () => {
|
|||||||
altLink1 = 'https://alternative.link/endpoint/1234';
|
altLink1 = 'https://alternative.link/endpoint/1234';
|
||||||
altLink2 = 'https://alternative.link/endpoint/5678';
|
altLink2 = 'https://alternative.link/endpoint/5678';
|
||||||
requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
||||||
|
requestUUID2 = 'c0f486c1-c4d3-4a03-b293-ca5b71ff0054';
|
||||||
alternativeLink = 'https://rest.api/endpoint/5e4f8a5-be98-4c51-9fd8-6bfedcbd59b7/item';
|
alternativeLink = 'https://rest.api/endpoint/5e4f8a5-be98-4c51-9fd8-6bfedcbd59b7/item';
|
||||||
timestamp = new Date().getTime();
|
timestamp = new Date().getTime();
|
||||||
timestamp2 = new Date().getTime() - 200;
|
timestamp2 = new Date().getTime() - 200;
|
||||||
@@ -71,13 +76,17 @@ describe('ObjectCacheService', () => {
|
|||||||
data: objectToCache,
|
data: objectToCache,
|
||||||
timeCompleted: timestamp,
|
timeCompleted: timestamp,
|
||||||
msToLive: msToLive,
|
msToLive: msToLive,
|
||||||
alternativeLinks: [altLink1, altLink2]
|
alternativeLinks: [altLink1, altLink2],
|
||||||
|
requestUUIDs: [requestUUID],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
};
|
};
|
||||||
cacheEntry2 = {
|
cacheEntry2 = {
|
||||||
data: objectToCache,
|
data: objectToCache,
|
||||||
timeCompleted: timestamp2,
|
timeCompleted: timestamp2,
|
||||||
msToLive: msToLive2,
|
msToLive: msToLive2,
|
||||||
alternativeLinks: [altLink2]
|
alternativeLinks: [altLink2],
|
||||||
|
requestUUIDs: [requestUUID2],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
};
|
};
|
||||||
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
||||||
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||||
@@ -343,4 +352,122 @@ describe('ObjectCacheService', () => {
|
|||||||
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
|
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('request dependencies', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const state = Object.assign({}, initialState, {
|
||||||
|
core: Object.assign({}, initialState.core, {
|
||||||
|
'cache/object': {
|
||||||
|
['objectWithoutDependents']: {
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
|
},
|
||||||
|
['objectWithDependents']: {
|
||||||
|
dependentRequestUUIDs: [requestUUID],
|
||||||
|
},
|
||||||
|
[selfLink]: cacheEntry,
|
||||||
|
},
|
||||||
|
'index': {
|
||||||
|
'object/alt-link-to-self-link': {
|
||||||
|
[anotherLink]: selfLink,
|
||||||
|
['objectWithoutDependentsAlt']: 'objectWithoutDependents',
|
||||||
|
['objectWithDependentsAlt']: 'objectWithDependents',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mockStore.setState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addDependency', () => {
|
||||||
|
it('should dispatch an ADD_DEPENDENTS action', () => {
|
||||||
|
service.addDependency(selfLink, 'objectWithoutDependents');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve alt links', () => {
|
||||||
|
service.addDependency(anotherLink, 'objectWithoutDependentsAlt');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch if either href cannot be resolved to a cached self link', () => {
|
||||||
|
service.addDependency(selfLink, 'unknown');
|
||||||
|
service.addDependency('unknown', 'objectWithoutDependents');
|
||||||
|
service.addDependency('nothing', 'matches');
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch if either href is undefined', () => {
|
||||||
|
service.addDependency(selfLink, undefined);
|
||||||
|
service.addDependency(undefined, 'objectWithoutDependents');
|
||||||
|
service.addDependency(undefined, undefined);
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch if the dependency exists already', () => {
|
||||||
|
service.addDependency(selfLink, 'objectWithDependents');
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with observable hrefs', () => {
|
||||||
|
service.addDependency(observableOf(selfLink), observableOf('objectWithoutDependents'));
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only dispatch once for the first value of either observable href', () => {
|
||||||
|
const testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
testScheduler.run(({ cold: tsCold, flush }) => {
|
||||||
|
const href$ = tsCold('--y-n-n', {
|
||||||
|
y: selfLink,
|
||||||
|
n: 'NOPE'
|
||||||
|
});
|
||||||
|
const dependsOnHref$ = tsCold('-y-n-n', {
|
||||||
|
y: 'objectWithoutDependents',
|
||||||
|
n: 'NOPE'
|
||||||
|
});
|
||||||
|
|
||||||
|
service.addDependency(href$, dependsOnHref$);
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch if either of the hrefs emits undefined', () => {
|
||||||
|
const testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
testScheduler.run(({ cold: tsCold, flush }) => {
|
||||||
|
const undefined$ = tsCold('--u');
|
||||||
|
|
||||||
|
service.addDependency(selfLink, undefined$);
|
||||||
|
service.addDependency(undefined$, 'objectWithoutDependents');
|
||||||
|
service.addDependency(undefined$, undefined$);
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeDependents', () => {
|
||||||
|
it('should dispatch a REMOVE_DEPENDENTS action', () => {
|
||||||
|
service.removeDependents('objectWithDependents');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new RemoveDependentsObjectCacheAction('objectWithDependents'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve alt links', () => {
|
||||||
|
service.removeDependents('objectWithDependentsAlt');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledOnceWith(new RemoveDependentsObjectCacheAction('objectWithDependents'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch if the href cannot be resolved to a cached self link', () => {
|
||||||
|
service.removeDependents('unknown');
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
107
src/app/core/cache/object-cache.service.ts
vendored
107
src/app/core/cache/object-cache.service.ts
vendored
@@ -4,23 +4,15 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
|||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import {
|
import { selfLinkFromAlternativeLinkSelector, selfLinkFromUuidSelector } from '../index/index.selectors';
|
||||||
selfLinkFromAlternativeLinkSelector,
|
|
||||||
selfLinkFromUuidSelector
|
|
||||||
} from '../index/index.selectors';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { getClassForType } from './builders/build-decorators';
|
import { getClassForType } from './builders/build-decorators';
|
||||||
import { LinkService } from './builders/link.service';
|
import { LinkService } from './builders/link.service';
|
||||||
import {
|
import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||||
AddPatchObjectCacheAction,
|
|
||||||
AddToObjectCacheAction,
|
|
||||||
ApplyPatchObjectCacheAction,
|
|
||||||
RemoveFromObjectCacheAction
|
|
||||||
} from './object-cache.actions';
|
|
||||||
|
|
||||||
import { ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
|
import { ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
|
||||||
import { AddToSSBAction } from './server-sync-buffer.actions';
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
@@ -339,4 +331,97 @@ export class ObjectCacheService {
|
|||||||
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
|
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new dependency between two cached objects.
|
||||||
|
* When {@link dependsOnHref$} is invalidated, {@link href$} will be invalidated as well.
|
||||||
|
*
|
||||||
|
* This method should be called _after_ requests have been sent;
|
||||||
|
* it will only work if both objects are already present in the cache.
|
||||||
|
*
|
||||||
|
* If either object is undefined, the dependency will not be added
|
||||||
|
*
|
||||||
|
* @param href$ the href of an object to add a dependency to
|
||||||
|
* @param dependsOnHref$ the href of the new dependency
|
||||||
|
*/
|
||||||
|
addDependency(href$: string | Observable<string>, dependsOnHref$: string | Observable<string>) {
|
||||||
|
if (hasNoValue(href$) || hasNoValue(dependsOnHref$)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof href$ === 'string') {
|
||||||
|
href$ = observableOf(href$);
|
||||||
|
}
|
||||||
|
if (typeof dependsOnHref$ === 'string') {
|
||||||
|
dependsOnHref$ = observableOf(dependsOnHref$);
|
||||||
|
}
|
||||||
|
|
||||||
|
observableCombineLatest([
|
||||||
|
href$,
|
||||||
|
dependsOnHref$.pipe(
|
||||||
|
switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref))
|
||||||
|
),
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([href, dependsOnSelfLink]: [string, string]) => {
|
||||||
|
const dependsOnSelfLink$ = observableOf(dependsOnSelfLink);
|
||||||
|
|
||||||
|
return observableCombineLatest([
|
||||||
|
dependsOnSelfLink$,
|
||||||
|
dependsOnSelfLink$.pipe(
|
||||||
|
switchMap(selfLink => this.getBySelfLink(selfLink)),
|
||||||
|
map(oce => oce?.dependentRequestUUIDs || []),
|
||||||
|
),
|
||||||
|
this.getByHref(href).pipe(
|
||||||
|
// only add the latest request to keep dependency index from growing indefinitely
|
||||||
|
map((entry: ObjectCacheEntry) => entry?.requestUUIDs?.[0]),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
).subscribe(([dependsOnSelfLink, currentDependents, newDependent]: [string, string[], string]) => {
|
||||||
|
// don't dispatch if either href is invalid or if the new dependency already exists
|
||||||
|
if (hasValue(dependsOnSelfLink) && hasValue(newDependent) && !currentDependents.includes(newDependent)) {
|
||||||
|
this.store.dispatch(new AddDependentsObjectCacheAction(dependsOnSelfLink, [newDependent]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all dependent requests associated with a cache entry.
|
||||||
|
*
|
||||||
|
* @href the href of a cached object
|
||||||
|
*/
|
||||||
|
removeDependents(href: string) {
|
||||||
|
this.resolveSelfLink(href).pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((selfLink: string) => {
|
||||||
|
if (hasValue(selfLink)) {
|
||||||
|
this.store.dispatch(new RemoveDependentsObjectCacheAction(selfLink));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the self link of an existing cached object from an arbitrary href
|
||||||
|
*
|
||||||
|
* @param href any href
|
||||||
|
* @return an observable of the self link corresponding to the given href.
|
||||||
|
* Will emit the given href if it was a self link, another href
|
||||||
|
* if the given href was an alt link, or undefined if there is no
|
||||||
|
* cached object for this href.
|
||||||
|
*/
|
||||||
|
private resolveSelfLink(href: string): Observable<string> {
|
||||||
|
return this.getBySelfLink(href).pipe(
|
||||||
|
switchMap((oce: ObjectCacheEntry) => {
|
||||||
|
if (isNotEmpty(oce)) {
|
||||||
|
return [href];
|
||||||
|
} else {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(selfLinkFromAlternativeLinkSelector(href)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s
|
|||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
@@ -20,6 +20,7 @@ import { RemoteData } from '../remote-data';
|
|||||||
import { RequestEntryState } from '../request-entry-state.model';
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { BaseDataService } from './base-data.service';
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -65,7 +66,13 @@ describe('BaseDataService', () => {
|
|||||||
},
|
},
|
||||||
getByHref: () => {
|
getByHref: () => {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
},
|
||||||
|
addDependency: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
removeDependents: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
linksToFollow = [
|
linksToFollow = [
|
||||||
@@ -558,7 +565,8 @@ describe('BaseDataService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
|
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
|
||||||
requestUUIDs: ['request1', 'request2', 'request3']
|
requestUUIDs: ['request1', 'request2', 'request3'],
|
||||||
|
dependentRequestUUIDs: ['request4', 'request5']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -570,6 +578,8 @@ describe('BaseDataService', () => {
|
|||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request4');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request5');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -582,6 +592,8 @@ describe('BaseDataService', () => {
|
|||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request4');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request5');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return an Observable that only emits true once all requests are stale', () => {
|
it('should return an Observable that only emits true once all requests are stale', () => {
|
||||||
@@ -591,9 +603,13 @@ describe('BaseDataService', () => {
|
|||||||
case 'request1':
|
case 'request1':
|
||||||
return cold('--(t|)', BOOLEAN);
|
return cold('--(t|)', BOOLEAN);
|
||||||
case 'request2':
|
case 'request2':
|
||||||
return cold('----(t|)', BOOLEAN);
|
|
||||||
case 'request3':
|
|
||||||
return cold('------(t|)', BOOLEAN);
|
return cold('------(t|)', BOOLEAN);
|
||||||
|
case 'request3':
|
||||||
|
return cold('---(t|)', BOOLEAN);
|
||||||
|
case 'request4':
|
||||||
|
return cold('-(t|)', BOOLEAN);
|
||||||
|
case 'request5':
|
||||||
|
return cold('----(t|)', BOOLEAN);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -607,9 +623,9 @@ describe('BaseDataService', () => {
|
|||||||
it('should only fire for the current state of the object (instead of tracking it)', () => {
|
it('should only fire for the current state of the object (instead of tracking it)', () => {
|
||||||
testScheduler.run(({ cold, flush }) => {
|
testScheduler.run(({ cold, flush }) => {
|
||||||
getByHrefSpy.and.returnValue(cold('a---b---c---', {
|
getByHrefSpy.and.returnValue(cold('a---b---c---', {
|
||||||
a: { requestUUIDs: ['request1'] }, // this is the state at the moment we're invalidating the cache
|
a: { requestUUIDs: ['request1'], dependentRequestUUIDs: [] }, // this is the state at the moment we're invalidating the cache
|
||||||
b: { requestUUIDs: ['request2'] }, // we shouldn't keep tracking the state
|
b: { requestUUIDs: ['request2'], dependentRequestUUIDs: [] }, // we shouldn't keep tracking the state
|
||||||
c: { requestUUIDs: ['request3'] }, // because we may invalidate when we shouldn't
|
c: { requestUUIDs: ['request3'], dependentRequestUUIDs: [] }, // because we may invalidate when we shouldn't
|
||||||
}));
|
}));
|
||||||
|
|
||||||
service.invalidateByHref('some-href');
|
service.invalidateByHref('some-href');
|
||||||
@@ -624,4 +640,42 @@ describe('BaseDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addDependency', () => {
|
||||||
|
let addDependencySpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
addDependencySpy = spyOn(objectCache, 'addDependency');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call objectCache.addDependency with the object\'s self link', () => {
|
||||||
|
addDependencySpy.and.callFake((href$: Observable<string>, dependsOn$: Observable<string>) => {
|
||||||
|
observableCombineLatest([href$, dependsOn$]).subscribe(([href, dependsOn]) => {
|
||||||
|
expect(href).toBe('object-href');
|
||||||
|
expect(dependsOn).toBe('dependsOnHref');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(service as any).addDependency(
|
||||||
|
createSuccessfulRemoteDataObject$({ _links: { self: { href: 'object-href' } } }),
|
||||||
|
observableOf('dependsOnHref')
|
||||||
|
);
|
||||||
|
expect(addDependencySpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call objectCache.addDependency without an href if request failed', () => {
|
||||||
|
addDependencySpy.and.callFake((href$: Observable<string>, dependsOn$: Observable<string>) => {
|
||||||
|
observableCombineLatest([href$, dependsOn$]).subscribe(([href, dependsOn]) => {
|
||||||
|
expect(href).toBe(undefined);
|
||||||
|
expect(dependsOn).toBe('dependsOnHref');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(service as any).addDependency(
|
||||||
|
createFailedRemoteDataObject$('something went wrong'),
|
||||||
|
observableOf('dependsOnHref')
|
||||||
|
);
|
||||||
|
expect(addDependencySpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -23,6 +23,7 @@ import { PaginatedList } from '../paginated-list.model';
|
|||||||
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
import { HALDataService } from './hal-data-service.interface';
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common functionality for data services.
|
* Common functionality for data services.
|
||||||
@@ -352,19 +353,55 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate a cached object by its href
|
* Shorthand method to add a dependency to a cached object
|
||||||
* @param href the href to invalidate
|
* ```
|
||||||
|
* const out$ = this.findByHref(...); // or another method that sends a request
|
||||||
|
* this.addDependency(out$, dependsOnHref);
|
||||||
|
* ```
|
||||||
|
* When {@link dependsOnHref$} is invalidated, {@link object$} will be invalidated as well.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param object$ the cached object
|
||||||
|
* @param dependsOnHref$ the href of the object it should depend on
|
||||||
*/
|
*/
|
||||||
public invalidateByHref(href: string): Observable<boolean> {
|
protected addDependency(object$: Observable<RemoteData<T | PaginatedList<T>>>, dependsOnHref$: string | Observable<string>) {
|
||||||
|
this.objectCache.addDependency(
|
||||||
|
object$.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<T>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return [rd.payload._links.self.href];
|
||||||
|
} else {
|
||||||
|
// undefined href will be skipped in objectCache.addDependency
|
||||||
|
return [undefined];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
dependsOnHref$
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
|
||||||
|
* @param href The self link of the object to be invalidated
|
||||||
|
* @return An Observable that will emit `true` once all requests are stale
|
||||||
|
*/
|
||||||
|
invalidateByHref(href: string): Observable<boolean> {
|
||||||
const done$ = new AsyncSubject<boolean>();
|
const done$ = new AsyncSubject<boolean>();
|
||||||
|
|
||||||
this.objectCache.getByHref(href).pipe(
|
this.objectCache.getByHref(href).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
|
switchMap((oce: ObjectCacheEntry) => {
|
||||||
|
return observableFrom([
|
||||||
|
...oce.requestUUIDs,
|
||||||
|
...oce.dependentRequestUUIDs
|
||||||
|
]).pipe(
|
||||||
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
||||||
toArray(),
|
toArray(),
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
).subscribe(() => {
|
).subscribe(() => {
|
||||||
|
this.objectCache.removeDependents(href);
|
||||||
done$.next(true);
|
done$.next(true);
|
||||||
done$.complete();
|
done$.complete();
|
||||||
});
|
});
|
||||||
|
@@ -22,7 +22,7 @@ import { RequestEntryState } from '../request-entry-state.model';
|
|||||||
import { DeleteData, DeleteDataImpl } from './delete-data';
|
import { DeleteData, DeleteDataImpl } from './delete-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { RestRequestMethod } from '../rest-request-method';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it
|
* Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it
|
||||||
@@ -63,8 +63,6 @@ export function testDeleteDataImplementation(serviceFactory: () => DeleteData<an
|
|||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
const BOOLEAN = { f: false, t: true };
|
|
||||||
|
|
||||||
class TestService extends DeleteDataImpl<any> {
|
class TestService extends DeleteDataImpl<any> {
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -155,13 +153,13 @@ describe('DeleteDataImpl', () => {
|
|||||||
let MOCK_FAILED_RD;
|
let MOCK_FAILED_RD;
|
||||||
|
|
||||||
let invalidateByHrefSpy: jasmine.Spy;
|
let invalidateByHrefSpy: jasmine.Spy;
|
||||||
let buildFromRequestUUIDSpy: jasmine.Spy;
|
let buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
|
||||||
let getIDHrefObsSpy: jasmine.Spy;
|
let getIDHrefObsSpy: jasmine.Spy;
|
||||||
let deleteByHrefSpy: jasmine.Spy;
|
let deleteByHrefSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
||||||
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
|
buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
|
||||||
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
|
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
|
||||||
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
|
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
|
||||||
|
|
||||||
@@ -171,7 +169,7 @@ describe('DeleteDataImpl', () => {
|
|||||||
|
|
||||||
it('should retrieve href by ID and call deleteByHref', () => {
|
it('should retrieve href by ID and call deleteByHref', () => {
|
||||||
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
|
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
|
||||||
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
|
||||||
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
|
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
|
||||||
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
|
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
|
||||||
@@ -180,66 +178,53 @@ describe('DeleteDataImpl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteByHref', () => {
|
describe('deleteByHref', () => {
|
||||||
it('should call invalidateByHref if the DELETE request succeeds', (done) => {
|
it('should send a DELETE request', (done) => {
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.deleteByHref('some-href').subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.DELETE,
|
||||||
|
href: 'some-href',
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include the virtual metadata to be copied in the DELETE request', (done) => {
|
||||||
|
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.deleteByHref('some-href', ['a', 'b', 'c']).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.DELETE,
|
||||||
|
href: 'some-href?copyVirtualMetadata=a©VirtualMetadata=b©VirtualMetadata=c',
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invalidate the currently cached object', (done) => {
|
||||||
|
service.deleteByHref('some-href').subscribe(() => {
|
||||||
|
expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalledWith(
|
||||||
|
requestService.generateRequestId(),
|
||||||
|
jasmine.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the RemoteData of the response', (done) => {
|
||||||
|
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
service.deleteByHref('some-href').subscribe(rd => {
|
service.deleteByHref('some-href').subscribe(rd => {
|
||||||
expect(rd).toBe(MOCK_SUCCEEDED_RD);
|
expect(rd).toBe(MOCK_SUCCEEDED_RD);
|
||||||
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
|
||||||
|
|
||||||
service.deleteByHref('some-href');
|
|
||||||
tick();
|
|
||||||
|
|
||||||
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not call invalidateByHref if the DELETE request fails', (done) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
|
|
||||||
|
|
||||||
service.deleteByHref('some-href').subscribe(rd => {
|
|
||||||
expect(rd).toBe(MOCK_FAILED_RD);
|
|
||||||
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wait for invalidateByHref before emitting', () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(
|
|
||||||
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
|
|
||||||
);
|
|
||||||
invalidateByHrefSpy.and.returnValue(
|
|
||||||
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
|
|
||||||
);
|
|
||||||
|
|
||||||
const done$ = service.deleteByHref('some-href');
|
|
||||||
expectObservable(done$).toBe(
|
|
||||||
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wait for the DELETE request to resolve before emitting', () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(
|
|
||||||
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
|
|
||||||
);
|
|
||||||
invalidateByHrefSpy.and.returnValue(
|
|
||||||
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
|
|
||||||
); // e.g.: maybe already stale before this call?
|
|
||||||
|
|
||||||
const done$ = service.deleteByHref('some-href');
|
|
||||||
expectObservable(done$).toBe(
|
|
||||||
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,13 +6,12 @@
|
|||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
import { CacheableObject } from '../../cache/cacheable-object.model';
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
import { AsyncSubject, combineLatest, Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { NoContent } from '../../shared/NoContent.model';
|
import { NoContent } from '../../shared/NoContent.model';
|
||||||
import { filter, map, switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { DeleteRequest } from '../request.models';
|
import { DeleteRequest } from '../request.models';
|
||||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
@@ -83,26 +82,6 @@ export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataS
|
|||||||
}
|
}
|
||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
|
|
||||||
const response$ = this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
|
||||||
|
|
||||||
const invalidated$ = new AsyncSubject<boolean>();
|
|
||||||
response$.pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
switchMap((rd: RemoteData<NoContent>) => {
|
|
||||||
if (rd.hasSucceeded) {
|
|
||||||
return this.invalidateByHref(href);
|
|
||||||
} else {
|
|
||||||
return [true];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).subscribe(() => {
|
|
||||||
invalidated$.next(true);
|
|
||||||
invalidated$.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
return combineLatest([response$, invalidated$]).pipe(
|
|
||||||
filter(([_, invalidated]) => invalidated),
|
|
||||||
map(([response, _]) => response),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -178,7 +178,12 @@ describe('PatchDataImpl', () => {
|
|||||||
|
|
||||||
describe('patch', () => {
|
describe('patch', () => {
|
||||||
const dso = {
|
const dso = {
|
||||||
uuid: 'dso-uuid'
|
uuid: 'dso-uuid',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'dso-href',
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const operations = [
|
const operations = [
|
||||||
Object.assign({
|
Object.assign({
|
||||||
@@ -188,14 +193,23 @@ describe('PatchDataImpl', () => {
|
|||||||
}) as Operation
|
}) as Operation
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach((done) => {
|
it('should send a PatchRequest', () => {
|
||||||
service.patch(dso, operations).subscribe(() => {
|
service.patch(dso, operations);
|
||||||
done();
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a PatchRequest', () => {
|
it('should invalidate the cached object if successfully patched', () => {
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
spyOn(rdbService, 'buildFromRequestUUIDAndAwait');
|
||||||
|
spyOn(service, 'invalidateByHref');
|
||||||
|
|
||||||
|
service.patch(dso, operations);
|
||||||
|
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect((rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('dso-href');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -101,7 +101,7 @@ export class PatchDataImpl<T extends CacheableObject> extends IdentifiableDataSe
|
|||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(object._links.self.href));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -54,7 +54,8 @@ describe('BitstreamFormatDataService', () => {
|
|||||||
function initTestService(halService) {
|
function initTestService(halService) {
|
||||||
rd = createSuccessfulRemoteDataObject({});
|
rd = createSuccessfulRemoteDataObject({});
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildFromRequestUUID: observableOf(rd)
|
buildFromRequestUUID: observableOf(rd),
|
||||||
|
buildFromRequestUUIDAndAwait: observableOf(rd),
|
||||||
});
|
});
|
||||||
|
|
||||||
return new BitstreamFormatDataService(
|
return new BitstreamFormatDataService(
|
||||||
|
@@ -2,7 +2,7 @@ import { AuthorizationDataService } from './authorization-data.service';
|
|||||||
import { SiteDataService } from '../site-data.service';
|
import { SiteDataService } from '../site-data.service';
|
||||||
import { Site } from '../../shared/site.model';
|
import { Site } from '../../shared/site.model';
|
||||||
import { EPerson } from '../../eperson/models/eperson.model';
|
import { EPerson } from '../../eperson/models/eperson.model';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { FeatureID } from './feature-id';
|
import { FeatureID } from './feature-id';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
@@ -12,10 +12,12 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
|||||||
import { Feature } from '../../shared/feature.model';
|
import { Feature } from '../../shared/feature.model';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
import { testSearchDataImplementation } from '../base/search-data.spec';
|
import { testSearchDataImplementation } from '../base/search-data.spec';
|
||||||
|
import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock';
|
||||||
|
|
||||||
describe('AuthorizationDataService', () => {
|
describe('AuthorizationDataService', () => {
|
||||||
let service: AuthorizationDataService;
|
let service: AuthorizationDataService;
|
||||||
let siteService: SiteDataService;
|
let siteService: SiteDataService;
|
||||||
|
let objectCache;
|
||||||
|
|
||||||
let site: Site;
|
let site: Site;
|
||||||
let ePerson: EPerson;
|
let ePerson: EPerson;
|
||||||
@@ -38,7 +40,8 @@ describe('AuthorizationDataService', () => {
|
|||||||
siteService = jasmine.createSpyObj('siteService', {
|
siteService = jasmine.createSpyObj('siteService', {
|
||||||
find: observableOf(site),
|
find: observableOf(site),
|
||||||
});
|
});
|
||||||
service = new AuthorizationDataService(requestService, undefined, undefined, undefined, siteService);
|
objectCache = getMockObjectCacheService();
|
||||||
|
service = new AuthorizationDataService(requestService, undefined, objectCache, undefined, siteService);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -110,6 +113,43 @@ describe('AuthorizationDataService', () => {
|
|||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true, true);
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dependencies', () => {
|
||||||
|
let addDependencySpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(service.searchBy as any).and.returnValue(observableOf('searchBy RD$'));
|
||||||
|
addDependencySpy = spyOn(service as any, 'addDependency');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a dependency on the objectUrl', (done) => {
|
||||||
|
addDependencySpy.and.callFake((href$: Observable<string>, dependsOn$: Observable<string>) => {
|
||||||
|
observableCombineLatest([href$, dependsOn$]).subscribe(([href, dependsOn]) => {
|
||||||
|
expect(href).toBe('searchBy RD$');
|
||||||
|
expect(dependsOn).toBe('object-href');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
service.searchByObject(FeatureID.AdministratorOf, 'object-href').subscribe(() => {
|
||||||
|
expect(addDependencySpy).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a dependency on the Site object if no objectUrl is given', (done) => {
|
||||||
|
addDependencySpy.and.callFake((object$: Observable<any>, dependsOn$: Observable<string>) => {
|
||||||
|
observableCombineLatest([object$, dependsOn$]).subscribe(([object, dependsOn]) => {
|
||||||
|
expect(object).toBe('searchBy RD$');
|
||||||
|
expect(dependsOn).toBe('test-site-href');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
service.searchByObject(FeatureID.AdministratorOf).subscribe(() => {
|
||||||
|
expect(addDependencySpy).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isAuthorized', () => {
|
describe('isAuthorized', () => {
|
||||||
|
@@ -11,10 +11,10 @@ import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-
|
|||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { PaginatedList } from '../paginated-list.model';
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||||
import { addSiteObjectUrlIfEmpty, oneAuthorizationMatchesFeature } from './authorization-utils';
|
import { oneAuthorizationMatchesFeature } from './authorization-utils';
|
||||||
import { FeatureID } from './feature-id';
|
import { FeatureID } from './feature-id';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
@@ -96,12 +96,28 @@ export class AuthorizationDataService extends BaseDataService<Authorization> imp
|
|||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
|
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
|
||||||
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
const objectUrl$ = observableOf(objectUrl).pipe(
|
||||||
addSiteObjectUrlIfEmpty(this.siteService),
|
switchMap((url) => {
|
||||||
|
if (hasNoValue(url)) {
|
||||||
|
return this.siteService.find().pipe(
|
||||||
|
map((site) => site.self)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(url);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const out$ = objectUrl$.pipe(
|
||||||
|
map((url: string) => new AuthorizationSearchParams(url, ePersonUuid, featureId)),
|
||||||
switchMap((params: AuthorizationSearchParams) => {
|
switchMap((params: AuthorizationSearchParams) => {
|
||||||
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addDependency(out$, objectUrl$);
|
||||||
|
|
||||||
|
return out$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,4 +11,5 @@ export class FindListOptions {
|
|||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
searchParams?: RequestParam[];
|
searchParams?: RequestParam[];
|
||||||
startsWith?: string;
|
startsWith?: string;
|
||||||
|
fetchThumbnail?: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,8 @@ import { dataService } from '../base/data-service.decorator';
|
|||||||
|
|
||||||
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
||||||
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
||||||
|
export const BATCH_IMPORT_SCRIPT_NAME = 'import';
|
||||||
|
export const BATCH_EXPORT_SCRIPT_NAME = 'export';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(SCRIPT)
|
@dataService(SCRIPT)
|
||||||
|
@@ -202,6 +202,7 @@ describe('RelationshipDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
|
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
|
||||||
|
mockOptions = Object.assign(mockOptions, { fetchThumbnail: true });
|
||||||
service.getRelatedItemsByLabel(
|
service.getRelatedItemsByLabel(
|
||||||
mockItem,
|
mockItem,
|
||||||
mockLabel,
|
mockLabel,
|
||||||
@@ -213,8 +214,8 @@ describe('RelationshipDataService', () => {
|
|||||||
mockOptions,
|
mockOptions,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
followLink('leftItem'),
|
followLink('leftItem',{}, followLink('thumbnail')),
|
||||||
followLink('rightItem'),
|
followLink('rightItem',{}, followLink('thumbnail')),
|
||||||
followLink('relationshipType')
|
followLink('relationshipType')
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
|
@@ -45,6 +45,7 @@ import { SearchData, SearchDataImpl } from './base/search-data';
|
|||||||
import { PutData, PutDataImpl } from './base/put-data';
|
import { PutData, PutDataImpl } from './base/put-data';
|
||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { dataService } from './base/data-service.decorator';
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
import { itemLinksToFollow } from '../../shared/utils/relation-query.utils';
|
||||||
|
|
||||||
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
]).pipe(
|
]).pipe(
|
||||||
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe(() => this.itemService.findByHref(item._links.self.href, false));
|
).subscribe(() => this.itemService.findByHref(item._links.self.href));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,7 +259,10 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid));
|
let linksToFollow: FollowLinkConfig<Relationship>[] = itemLinksToFollow(options.fetchThumbnail);
|
||||||
|
linksToFollow.push(followLink('relationshipType'));
|
||||||
|
|
||||||
|
return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -516,14 +520,14 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
{
|
{
|
||||||
fieldName: 'relatedItem',
|
fieldName: 'relatedItem',
|
||||||
fieldValue: itemId,
|
fieldValue: itemId,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.searchBy(
|
return this.searchBy(
|
||||||
'byItemsAndType',
|
'byItemsAndType',
|
||||||
{
|
{
|
||||||
searchParams: searchParams,
|
searchParams: searchParams
|
||||||
},
|
},
|
||||||
) as Observable<RemoteData<PaginatedList<Relationship>>>;
|
) as Observable<RemoteData<PaginatedList<Relationship>>>;
|
||||||
|
|
||||||
|
@@ -307,7 +307,7 @@ describe('EPersonDataService', () => {
|
|||||||
it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => {
|
it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => {
|
||||||
service.patchPasswordWithToken('test-uuid', 'test-token', 'test-password');
|
service.patchPasswordWithToken('test-uuid', 'test-token', 'test-password');
|
||||||
|
|
||||||
const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' });
|
const operation = Object.assign({ op: 'add', path: '/password', value: { new_password: 'test-password' } });
|
||||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
|
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
|
||||||
|
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||||
|
@@ -3,7 +3,10 @@ import { createSelector, select, Store } from '@ngrx/store';
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find, map, take } from 'rxjs/operators';
|
import { find, map, take } from 'rxjs/operators';
|
||||||
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../access-control/epeople-registry/epeople-registry.actions';
|
import {
|
||||||
|
EPeopleRegistryCancelEPersonAction,
|
||||||
|
EPeopleRegistryEditEPersonAction
|
||||||
|
} from '../../access-control/epeople-registry/epeople-registry.actions';
|
||||||
import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
|
import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
@@ -318,7 +321,7 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RemoteData<EPerson>> {
|
patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RemoteData<EPerson>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const operation = Object.assign({ op: 'add', path: '/password', value: password });
|
const operation = Object.assign({ op: 'add', path: '/password', value: { 'new_password': password } });
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, uuid)),
|
map((endpoint: string) => this.getIDHref(endpoint, uuid)),
|
||||||
|
@@ -26,6 +26,9 @@ import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
|
|||||||
import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test';
|
import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
||||||
|
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
||||||
|
|
||||||
describe('GroupDataService', () => {
|
describe('GroupDataService', () => {
|
||||||
let service: GroupDataService;
|
let service: GroupDataService;
|
||||||
@@ -38,7 +41,7 @@ describe('GroupDataService', () => {
|
|||||||
let groups$;
|
let groups$;
|
||||||
let halService;
|
let halService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
function init() {
|
function init() {
|
||||||
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
|
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
|
||||||
groupsEndpoint = `${restEndpointURL}/groups`;
|
groupsEndpoint = `${restEndpointURL}/groups`;
|
||||||
@@ -46,6 +49,7 @@ describe('GroupDataService', () => {
|
|||||||
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
|
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
|
||||||
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
|
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
|
||||||
halService = new HALEndpointServiceStub(restEndpointURL);
|
halService = new HALEndpointServiceStub(restEndpointURL);
|
||||||
|
objectCache = getMockObjectCacheService();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -67,7 +71,7 @@ describe('GroupDataService', () => {
|
|||||||
return new GroupDataService(
|
return new GroupDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
null,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
new DummyChangeAnalyzer() as any,
|
new DummyChangeAnalyzer() as any,
|
||||||
null,
|
null,
|
||||||
@@ -82,6 +86,7 @@ describe('GroupDataService', () => {
|
|||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
|
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('searchGroups', () => {
|
describe('searchGroups', () => {
|
||||||
@@ -108,6 +113,11 @@ describe('GroupDataService', () => {
|
|||||||
|
|
||||||
describe('addSubGroupToGroup', () => {
|
describe('addSubGroupToGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
objectCache.getByHref.and.returnValue(observableOf({
|
||||||
|
requestUUIDs: ['request1', 'request2'],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
|
} as ObjectCacheEntry));
|
||||||
|
spyOn((service as any).deleteData, 'invalidateByHref');
|
||||||
service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe();
|
service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe();
|
||||||
});
|
});
|
||||||
it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => {
|
it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => {
|
||||||
@@ -118,20 +128,52 @@ describe('GroupDataService', () => {
|
|||||||
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options);
|
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options);
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
it('should invalidate the previous requests of the parent group', () => {
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteSubGroupFromGroup', () => {
|
describe('deleteSubGroupFromGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
objectCache.getByHref.and.returnValue(observableOf({
|
||||||
|
requestUUIDs: ['request1', 'request2'],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
|
} as ObjectCacheEntry));
|
||||||
|
spyOn((service as any).deleteData, 'invalidateByHref');
|
||||||
service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe();
|
service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe();
|
||||||
});
|
});
|
||||||
it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => {
|
it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => {
|
||||||
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id);
|
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id);
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
it('should invalidate the previous requests of the parent group\'', () => {
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addMemberToGroup', () => {
|
describe('addMemberToGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
objectCache.getByHref.and.returnValue(observableOf({
|
||||||
|
requestUUIDs: ['request1', 'request2'],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
|
} as ObjectCacheEntry));
|
||||||
|
spyOn((service as any).deleteData, 'invalidateByHref');
|
||||||
service.addMemberToGroup(GroupMock, EPersonMock2).subscribe();
|
service.addMemberToGroup(GroupMock, EPersonMock2).subscribe();
|
||||||
});
|
});
|
||||||
it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => {
|
it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => {
|
||||||
@@ -142,20 +184,49 @@ describe('GroupDataService', () => {
|
|||||||
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options);
|
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options);
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
it('should invalidate the previous requests of the EPerson and the group', () => {
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteMemberFromGroup', () => {
|
describe('deleteMemberFromGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
objectCache.getByHref.and.returnValue(observableOf({
|
||||||
|
requestUUIDs: ['request1', 'request2'],
|
||||||
|
dependentRequestUUIDs: [],
|
||||||
|
} as ObjectCacheEntry));
|
||||||
|
spyOn((service as any).deleteData, 'invalidateByHref');
|
||||||
service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe();
|
service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe();
|
||||||
});
|
});
|
||||||
it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => {
|
it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => {
|
||||||
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id);
|
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id);
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
it('should invalidate the previous requests of the EPerson and the group', () => {
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
|
||||||
|
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||||
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('editGroup', () => {
|
describe('editGroup', () => {
|
||||||
it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => {
|
it('should dispatch a EDIT_GROUP action with the group to start editing', () => {
|
||||||
service.editGroup(GroupMock);
|
service.editGroup(GroupMock);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock));
|
expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock));
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@ import { HttpHeaders } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, zip as observableZip } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
GroupRegistryCancelGroupAction,
|
GroupRegistryCancelGroupAction,
|
||||||
@@ -124,7 +124,8 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds given subgroup as a subgroup to the given active group
|
* Adds given subgroup as a subgroup to the given active group and waits until the {@link activeGroup} and
|
||||||
|
* the {@link subgroup} are invalidated.
|
||||||
* @param activeGroup Group we want to add subgroup to
|
* @param activeGroup Group we want to add subgroup to
|
||||||
* @param subgroup Group we want to add as subgroup to activeGroup
|
* @param subgroup Group we want to add as subgroup to activeGroup
|
||||||
*/
|
*/
|
||||||
@@ -137,11 +138,16 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options);
|
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options);
|
||||||
this.requestService.send(postRequest);
|
this.requestService.send(postRequest);
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
|
this.invalidateByHref(activeGroup._links.self.href),
|
||||||
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a given subgroup from the subgroups of the given active group
|
* Deletes a given subgroup from the subgroups of the given active group and waits until the {@link activeGroup} and
|
||||||
|
* the {@link subgroup} are invalidated.
|
||||||
|
* are invalidated.
|
||||||
* @param activeGroup Group we want to delete subgroup from
|
* @param activeGroup Group we want to delete subgroup from
|
||||||
* @param subgroup Subgroup we want to delete from activeGroup
|
* @param subgroup Subgroup we want to delete from activeGroup
|
||||||
*/
|
*/
|
||||||
@@ -150,11 +156,15 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id);
|
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id);
|
||||||
this.requestService.send(deleteRequest);
|
this.requestService.send(deleteRequest);
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
|
this.invalidateByHref(activeGroup._links.self.href),
|
||||||
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds given ePerson as member to given group
|
* Adds given ePerson as member to a given group and invalidates the ePerson and waits until the {@link ePerson} and
|
||||||
|
* the {@link activeGroup} are invalidated.
|
||||||
* @param activeGroup Group we want to add member to
|
* @param activeGroup Group we want to add member to
|
||||||
* @param ePerson EPerson we want to add as member to given activeGroup
|
* @param ePerson EPerson we want to add as member to given activeGroup
|
||||||
*/
|
*/
|
||||||
@@ -167,11 +177,17 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options);
|
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options);
|
||||||
this.requestService.send(postRequest);
|
this.requestService.send(postRequest);
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
|
this.invalidateByHref(ePerson._links.self.href),
|
||||||
|
this.invalidateByHref(activeGroup._links.self.href),
|
||||||
|
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||||
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a given ePerson from the members of the given active group
|
* Deletes a given ePerson from the members of the given active group and waits until the {@link ePerson} and the
|
||||||
|
* {@link activeGroup} are invalidated.
|
||||||
* @param activeGroup Group we want to delete member from
|
* @param activeGroup Group we want to delete member from
|
||||||
* @param ePerson EPerson we want to delete from members of given activeGroup
|
* @param ePerson EPerson we want to delete from members of given activeGroup
|
||||||
*/
|
*/
|
||||||
@@ -180,7 +196,12 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id);
|
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id);
|
||||||
this.requestService.send(deleteRequest);
|
this.requestService.send(deleteRequest);
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
|
this.invalidateByHref(ePerson._links.self.href),
|
||||||
|
this.invalidateByHref(activeGroup._links.self.href),
|
||||||
|
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||||
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -19,6 +19,7 @@ import { RequestEntry } from '../data/request-entry.model';
|
|||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../eperson/group-data.service';
|
import { GroupDataService } from '../eperson/group-data.service';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
describe('ResourcePolicyService', () => {
|
describe('ResourcePolicyService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -120,12 +121,23 @@ describe('ResourcePolicyService', () => {
|
|||||||
}),
|
}),
|
||||||
buildFromRequestUUID: hot('a|', {
|
buildFromRequestUUID: hot('a|', {
|
||||||
a: resourcePolicyRD
|
a: resourcePolicyRD
|
||||||
|
}),
|
||||||
|
buildFromRequestUUIDAndAwait: hot('a|', {
|
||||||
|
a: resourcePolicyRD
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
ePersonService = jasmine.createSpyObj('ePersonService', {
|
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||||
getBrowseEndpoint: hot('a', {
|
getBrowseEndpoint: hot('a', {
|
||||||
a: ePersonEndpoint
|
a: ePersonEndpoint
|
||||||
}),
|
}),
|
||||||
|
getIDHrefObs: cold('a', {
|
||||||
|
a: 'https://rest.api/rest/api/eperson/epersons/' + epersonUUID
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
groupService = jasmine.createSpyObj('groupService', {
|
||||||
|
getIDHrefObs: cold('a', {
|
||||||
|
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = {} as ObjectCacheService;
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
@@ -142,11 +154,12 @@ describe('ResourcePolicyService', () => {
|
|||||||
groupService,
|
groupService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
spyOn(service, 'findById').and.callThrough();
|
||||||
|
spyOn(service, 'findByHref').and.callThrough();
|
||||||
|
spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
||||||
spyOn((service as any).createData, 'create').and.callThrough();
|
spyOn((service as any).createData, 'create').and.callThrough();
|
||||||
spyOn((service as any).deleteData, 'delete').and.callThrough();
|
spyOn((service as any).deleteData, 'delete').and.callThrough();
|
||||||
spyOn((service as any).patchData, 'update').and.callThrough();
|
spyOn((service as any).patchData, 'update').and.callThrough();
|
||||||
spyOn((service as any), 'findById').and.callThrough();
|
|
||||||
spyOn((service as any), 'findByHref').and.callThrough();
|
|
||||||
spyOn((service as any).searchData, 'searchBy').and.callThrough();
|
spyOn((service as any).searchData, 'searchBy').and.callThrough();
|
||||||
spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(observableOf(requestURL));
|
spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(observableOf(requestURL));
|
||||||
});
|
});
|
||||||
@@ -318,14 +331,32 @@ describe('ResourcePolicyService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateTarget', () => {
|
describe('updateTarget', () => {
|
||||||
it('should create a new PUT request for eperson', () => {
|
beforeEach(() => {
|
||||||
const targetType = 'eperson';
|
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID));
|
||||||
|
|
||||||
const result = service.updateTarget(resourcePolicyId, requestURL, epersonUUID, targetType);
|
|
||||||
const expected = cold('a|', {
|
|
||||||
a: resourcePolicyRD
|
|
||||||
});
|
});
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
|
it('should send a PUT request to update the EPerson', () => {
|
||||||
|
service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson');
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
uuid: requestUUID,
|
||||||
|
href: `${resourcePolicy._links.self.href}/eperson`,
|
||||||
|
body: 'https://rest.api/rest/api/eperson/epersons/' + epersonUUID,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invalidate the ResourcePolicy', () => {
|
||||||
|
service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson');
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
|
||||||
|
expect((rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
|
||||||
|
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith(resourcePolicy._links.self.href);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ import { PaginatedList } from '../data/paginated-list.model';
|
|||||||
import { ActionType } from './models/action-type.model';
|
import { ActionType } from './models/action-type.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { first, map } from 'rxjs/operators';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
@@ -194,13 +194,8 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
|
|||||||
* @param targetType the type of the target (eperson or group) to which the permission is being granted
|
* @param targetType the type of the target (eperson or group) to which the permission is being granted
|
||||||
*/
|
*/
|
||||||
updateTarget(resourcePolicyId: string, resourcePolicyHref: string, targetUUID: string, targetType: string): Observable<RemoteData<any>> {
|
updateTarget(resourcePolicyId: string, resourcePolicyHref: string, targetUUID: string, targetType: string): Observable<RemoteData<any>> {
|
||||||
|
|
||||||
const targetService = targetType === 'eperson' ? this.ePersonService : this.groupService;
|
const targetService = targetType === 'eperson' ? this.ePersonService : this.groupService;
|
||||||
|
const targetEndpoint$ = targetService.getIDHrefObs(targetUUID);
|
||||||
const targetEndpoint$ = targetService.getBrowseEndpoint().pipe(
|
|
||||||
take(1),
|
|
||||||
map((endpoint: string) =>`${endpoint}/${targetUUID}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -209,9 +204,9 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
|
|||||||
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
this.requestService.setStaleByHrefSubstring(`${this.getLinkPath()}/${resourcePolicyId}/${targetType}`);
|
targetEndpoint$.pipe(
|
||||||
|
first(),
|
||||||
targetEndpoint$.subscribe((targetEndpoint) => {
|
).subscribe((targetEndpoint) => {
|
||||||
const resourceEndpoint = resourcePolicyHref + '/' + targetType;
|
const resourceEndpoint = resourcePolicyHref + '/' + targetType;
|
||||||
const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options);
|
const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options);
|
||||||
Object.assign(request, {
|
Object.assign(request, {
|
||||||
@@ -222,8 +217,7 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
|
|||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(resourcePolicyHref));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,17 +2,25 @@ import { TestBed } from '@angular/core/testing';
|
|||||||
import { BrowserHardRedirectService } from './browser-hard-redirect.service';
|
import { BrowserHardRedirectService } from './browser-hard-redirect.service';
|
||||||
|
|
||||||
describe('BrowserHardRedirectService', () => {
|
describe('BrowserHardRedirectService', () => {
|
||||||
const origin = 'https://test-host.com:4000';
|
let origin: string;
|
||||||
const mockLocation = {
|
let mockLocation: Location;
|
||||||
|
let service: BrowserHardRedirectService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
origin = 'https://test-host.com:4000';
|
||||||
|
mockLocation = {
|
||||||
href: undefined,
|
href: undefined,
|
||||||
pathname: '/pathname',
|
pathname: '/pathname',
|
||||||
search: '/search',
|
search: '/search',
|
||||||
origin
|
origin,
|
||||||
|
replace: (url: string) => {
|
||||||
|
mockLocation.href = url;
|
||||||
|
}
|
||||||
} as Location;
|
} as Location;
|
||||||
|
spyOn(mockLocation, 'replace');
|
||||||
|
|
||||||
const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation);
|
service = new BrowserHardRedirectService(mockLocation);
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,8 +36,8 @@ describe('BrowserHardRedirectService', () => {
|
|||||||
service.redirect(redirect);
|
service.redirect(redirect);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the location', () => {
|
it('should call location.replace with the new url', () => {
|
||||||
expect(mockLocation.href).toEqual(redirect);
|
expect(mockLocation.replace).toHaveBeenCalledWith(redirect);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ export class BrowserHardRedirectService extends HardRedirectService {
|
|||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
redirect(url: string) {
|
redirect(url: string) {
|
||||||
this.location.href = url;
|
this.location.replace(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -40,6 +40,12 @@ export class BrowseEntry extends ListableObject implements TypedObject {
|
|||||||
@autoserializeAs('valueLang')
|
@autoserializeAs('valueLang')
|
||||||
language: string;
|
language: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail link used when browsing items with showThumbs config enabled.
|
||||||
|
*/
|
||||||
|
@autoserializeAs('thumbnail')
|
||||||
|
thumbnail: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The count of this browse entry
|
* The count of this browse entry
|
||||||
*/
|
*/
|
||||||
@@ -51,6 +57,7 @@ export class BrowseEntry extends ListableObject implements TypedObject {
|
|||||||
_links: {
|
_links: {
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
entries: HALLink;
|
entries: HALLink;
|
||||||
|
thumbnail: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -25,6 +25,7 @@ import { PaginationService } from '../../pagination/pagination.service';
|
|||||||
import { SearchConfigurationService } from './search-configuration.service';
|
import { SearchConfigurationService } from './search-configuration.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { RequestEntry } from '../../data/request-entry.model';
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
|
import { Angulartics2 } from 'angulartics2';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent {
|
class DummyComponent {
|
||||||
@@ -57,6 +58,7 @@ describe('SearchService', () => {
|
|||||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||||
{ provide: PaginationService, useValue: {} },
|
{ provide: PaginationService, useValue: {} },
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
||||||
|
{ provide: Angulartics2, useValue: {} },
|
||||||
SearchService
|
SearchService
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -124,6 +126,7 @@ describe('SearchService', () => {
|
|||||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
||||||
|
{ provide: Angulartics2, useValue: {} },
|
||||||
SearchService
|
SearchService
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@@ -33,6 +33,7 @@ import { SearchConfigurationService } from './search-configuration.service';
|
|||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { RestRequest } from '../../data/rest-request.model';
|
import { RestRequest } from '../../data/rest-request.model';
|
||||||
import { BaseDataService } from '../../data/base/base-data.service';
|
import { BaseDataService } from '../../data/base/base-data.service';
|
||||||
|
import { Angulartics2 } from 'angulartics2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A limited data service implementation for the 'discover' endpoint
|
* A limited data service implementation for the 'discover' endpoint
|
||||||
@@ -96,6 +97,7 @@ export class SearchService implements OnDestroy {
|
|||||||
private dspaceObjectService: DSpaceObjectDataService,
|
private dspaceObjectService: DSpaceObjectDataService,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private searchConfigurationService: SearchConfigurationService,
|
private searchConfigurationService: SearchConfigurationService,
|
||||||
|
private angulartics2: Angulartics2,
|
||||||
) {
|
) {
|
||||||
this.searchDataService = new SearchDataService();
|
this.searchDataService = new SearchDataService();
|
||||||
}
|
}
|
||||||
@@ -320,6 +322,37 @@ export class SearchService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send search event to rest api using angularitics
|
||||||
|
* @param config Paginated search options used
|
||||||
|
* @param searchQueryResponse The response objects of the performed search
|
||||||
|
*/
|
||||||
|
trackSearch(config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject>) {
|
||||||
|
const filters: { filter: string, operator: string, value: string, label: string; }[] = [];
|
||||||
|
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||||
|
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||||
|
const appliedFilter = appliedFilters[i];
|
||||||
|
filters.push(appliedFilter);
|
||||||
|
}
|
||||||
|
this.angulartics2.eventTrack.next({
|
||||||
|
action: 'search',
|
||||||
|
properties: {
|
||||||
|
searchOptions: config,
|
||||||
|
page: {
|
||||||
|
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
|
||||||
|
totalElements: searchQueryResponse.pageInfo.totalElements,
|
||||||
|
totalPages: searchQueryResponse.pageInfo.totalPages,
|
||||||
|
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
by: config.sort.field,
|
||||||
|
order: config.sort.direction
|
||||||
|
},
|
||||||
|
filters: filters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { SubmissionCcLicenseDataService } from './submission-cc-license-data.service';
|
||||||
|
import { testFindAllDataImplementation } from '../data/base/find-all-data.spec';
|
||||||
|
|
||||||
|
describe('SubmissionCcLicenseDataService', () => {
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new SubmissionCcLicenseDataService(null, null, null, null);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
});
|
||||||
|
});
|
@@ -6,7 +6,7 @@ import { RequestService } from '../data/request.service';
|
|||||||
import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type';
|
import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type';
|
||||||
import { SubmissionCcLicence } from './models/submission-cc-license.model';
|
import { SubmissionCcLicence } from './models/submission-cc-license.model';
|
||||||
import { BaseDataService } from '../data/base/base-data.service';
|
import { BaseDataService } from '../data/base/base-data.service';
|
||||||
import { FindAllData } from '../data/base/find-all-data';
|
import {FindAllData, FindAllDataImpl} from '../data/base/find-all-data';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -19,6 +19,7 @@ import { dataService } from '../data/base/data-service.decorator';
|
|||||||
export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCcLicence> implements FindAllData<SubmissionCcLicence> {
|
export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCcLicence> implements FindAllData<SubmissionCcLicence> {
|
||||||
|
|
||||||
protected linkPath = 'submissioncclicenses';
|
protected linkPath = 'submissioncclicenses';
|
||||||
|
private findAllData: FindAllData<SubmissionCcLicence>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -27,6 +28,8 @@ export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCc
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
) {
|
) {
|
||||||
super('submissioncclicenses', requestService, rdbService, objectCache, halService);
|
super('submissioncclicenses', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +47,6 @@ export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCc
|
|||||||
* Return an observable that emits object list
|
* Return an observable that emits object list
|
||||||
*/
|
*/
|
||||||
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCcLicence>[]): Observable<RemoteData<PaginatedList<SubmissionCcLicence>>> {
|
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCcLicence>[]): Observable<RemoteData<PaginatedList<SubmissionCcLicence>>> {
|
||||||
return undefined;
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="dsoTitle"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<p *ngIf="dso.hasMetadata('creativework.datePublished')"
|
<p *ngIf="dso.hasMetadata('creativework.datePublished')"
|
||||||
class="item-date card-text text-muted">
|
class="item-date card-text text-muted">
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="dsoTitle"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<p *ngIf="dso.hasMetadata('creativework.datePublished')"
|
<p *ngIf="dso.hasMetadata('creativework.datePublished')"
|
||||||
class="item-date card-text text-muted">
|
class="item-date card-text text-muted">
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="dsoTitle"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<p *ngIf="dso.hasMetadata('creativework.editor')"
|
<p *ngIf="dso.hasMetadata('creativework.editor')"
|
||||||
class="item-publisher card-text text-muted">
|
class="item-publisher card-text text-muted">
|
||||||
|
@@ -1,6 +1,17 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div *ngIf="showThumbnails" class="col-3 col-md-2">
|
||||||
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
|
||||||
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
|
</ds-thumbnail>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
|
rel="noopener noreferrer"
|
||||||
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
|
||||||
[innerHTML]="dsoTitle"></a>
|
[innerHTML]="dsoTitle"></a>
|
||||||
<span *ngIf="linkType == linkTypes.None"
|
<span *ngIf="linkType == linkTypes.None"
|
||||||
@@ -23,3 +34,5 @@
|
|||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -9,6 +9,7 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
|||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
let journalIssueListElementComponent: JournalIssueSearchResultListElementComponent;
|
let journalIssueListElementComponent: JournalIssueSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<JournalIssueSearchResultListElementComponent>;
|
let fixture: ComponentFixture<JournalIssueSearchResultListElementComponent>;
|
||||||
@@ -57,13 +58,26 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const environmentUseThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviromentNoThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('JournalIssueSearchResultListElementComponent', () => {
|
describe('JournalIssueSearchResultListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
|
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -78,6 +92,22 @@ describe('JournalIssueSearchResultListElementComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
journalIssueListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should set showThumbnails to true', () => {
|
||||||
|
expect(journalIssueListElementComponent.showThumbnails).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('When the item has a journal identifier', () => {
|
describe('When the item has a journal identifier', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
journalIssueListElementComponent.object = mockItemWithMetadata;
|
journalIssueListElementComponent.object = mockItemWithMetadata;
|
||||||
@@ -126,3 +156,39 @@ describe('JournalIssueSearchResultListElementComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('JournalIssueSearchResultListElementComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: TruncatableService, useValue: {}},
|
||||||
|
{provide: DSONameService, useClass: DSONameServiceMock},
|
||||||
|
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(JournalIssueSearchResultListElementComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture = TestBed.createComponent(JournalIssueSearchResultListElementComponent);
|
||||||
|
journalIssueListElementComponent = fixture.componentInstance;
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
journalIssueListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
|
|||||||
* The component for displaying a list element for an item search result of the type Journal Issue
|
* The component for displaying a list element for an item search result of the type Journal Issue
|
||||||
*/
|
*/
|
||||||
export class JournalIssueSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
export class JournalIssueSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display thumbnails if required by configuration
|
||||||
|
*/
|
||||||
|
showThumbnails: boolean;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,17 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div *ngIf="showThumbnails" class="col-3 col-md-2">
|
||||||
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
|
||||||
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
|
</ds-thumbnail>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
|
rel="noopener noreferrer"
|
||||||
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
|
||||||
[innerHTML]="dsoTitle"></a>
|
[innerHTML]="dsoTitle"></a>
|
||||||
<span *ngIf="linkType == linkTypes.None"
|
<span *ngIf="linkType == linkTypes.None"
|
||||||
@@ -23,3 +34,5 @@
|
|||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -9,6 +9,7 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
|||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
let journalVolumeListElementComponent: JournalVolumeSearchResultListElementComponent;
|
let journalVolumeListElementComponent: JournalVolumeSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<JournalVolumeSearchResultListElementComponent>;
|
let fixture: ComponentFixture<JournalVolumeSearchResultListElementComponent>;
|
||||||
@@ -56,6 +57,18 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const environmentUseThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviromentNoThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('JournalVolumeSearchResultListElementComponent', () => {
|
describe('JournalVolumeSearchResultListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -63,6 +76,7 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -77,6 +91,21 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
journalVolumeListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should set showThumbnails to true', () => {
|
||||||
|
expect(journalVolumeListElementComponent.showThumbnails).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('When the item has a journal title', () => {
|
describe('When the item has a journal title', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
journalVolumeListElementComponent.object = mockItemWithMetadata;
|
journalVolumeListElementComponent.object = mockItemWithMetadata;
|
||||||
@@ -125,3 +154,38 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('JournalVolumeSearchResultListElementComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [JournalVolumeSearchResultListElementComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: TruncatableService, useValue: {}},
|
||||||
|
{provide: DSONameService, useClass: DSONameServiceMock},
|
||||||
|
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(JournalVolumeSearchResultListElementComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture = TestBed.createComponent(JournalVolumeSearchResultListElementComponent);
|
||||||
|
journalVolumeListElementComponent = fixture.componentInstance;
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
journalVolumeListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
|
|||||||
* The component for displaying a list element for an item search result of the type Journal Volume
|
* The component for displaying a list element for an item search result of the type Journal Volume
|
||||||
*/
|
*/
|
||||||
export class JournalVolumeSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
export class JournalVolumeSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display thumbnails if required by configuration
|
||||||
|
*/
|
||||||
|
showThumbnails: boolean;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div *ngIf="showThumbnails" class="col-3 col-md-2">
|
||||||
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||||
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
|
||||||
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
|
</ds-thumbnail>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||||
@@ -17,3 +26,5 @@
|
|||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -9,6 +9,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
|
|||||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
let journalListElementComponent: JournalSearchResultListElementComponent;
|
let journalListElementComponent: JournalSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<JournalSearchResultListElementComponent>;
|
let fixture: ComponentFixture<JournalSearchResultListElementComponent>;
|
||||||
@@ -52,6 +53,18 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const environmentUseThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviromentNoThumbs = {
|
||||||
|
browseBy: {
|
||||||
|
showThumbnails: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('JournalSearchResultListElementComponent', () => {
|
describe('JournalSearchResultListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -59,6 +72,7 @@ describe('JournalSearchResultListElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -73,6 +87,21 @@ describe('JournalSearchResultListElementComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
journalListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should set showThumbnails to true', () => {
|
||||||
|
expect(journalListElementComponent.showThumbnails).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('When the item has an issn', () => {
|
describe('When the item has an issn', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
journalListElementComponent.object = mockItemWithMetadata;
|
journalListElementComponent.object = mockItemWithMetadata;
|
||||||
@@ -97,3 +126,39 @@ describe('JournalSearchResultListElementComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('JournalSearchResultListElementComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [JournalSearchResultListElementComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: TruncatableService, useValue: {}},
|
||||||
|
{provide: DSONameService, useClass: DSONameServiceMock},
|
||||||
|
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(JournalSearchResultListElementComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture = TestBed.createComponent(JournalSearchResultListElementComponent);
|
||||||
|
journalListElementComponent = fixture.componentInstance;
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('with environment.browseBy.showThumbnails set to false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
journalListElementComponent.object = mockItemWithMetadata;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add thumbnail element', () => {
|
||||||
|
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
|
||||||
|
expect(thumbnailElement).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
|
|||||||
* The component for displaying a list element for an item search result of the type Journal
|
* The component for displaying a list element for an item search result of the type Journal
|
||||||
*/
|
*/
|
||||||
export class JournalSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
export class JournalSearchResultListElementComponent extends ItemSearchResultListElementComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display thumbnails if required by configuration
|
||||||
|
*/
|
||||||
|
showThumbnails: boolean;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<h2 class="item-page-title-field mr-auto">
|
<ds-item-page-title-field [item]="object" class="mr-auto">
|
||||||
{{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
</ds-item-page-title-field>
|
||||||
</h2>
|
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
[tooltipMsgCreate]="'item.page.version.create'"
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<h2 class="item-page-title-field mr-auto">
|
<ds-item-page-title-field [item]="object" class="mr-auto">
|
||||||
{{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
</ds-item-page-title-field>
|
||||||
</h2>
|
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
[tooltipMsgCreate]="'item.page.version.create'"
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<h2 class="item-page-title-field mr-auto">
|
<ds-item-page-title-field [item]="object" class="mr-auto">
|
||||||
{{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
</ds-item-page-title-field>
|
||||||
</h2>
|
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
[tooltipMsgCreate]="'item.page.version.create'"
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user