mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 12:33:07 +00:00
Merge remote-tracking branch 'origin/main' into #1206
# Conflicts: # src/app/submission/edit/submission-edit.component.ts
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="(dsoRD$ | async)?.payload?.id"></ds-resource-policies>
|
||||||
|
<div class="button-row bottom">
|
||||||
|
<div class="text-right">
|
||||||
|
<a [routerLink]="['/bitstreams', (dsoRD$ | async)?.payload?.id, 'edit']" role="button" class="btn btn-outline-secondary mr-1">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{'bitstream.edit.return' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations.component';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('BitstreamAuthorizationsComponent', () => {
|
||||||
|
let comp: BitstreamAuthorizationsComponent<DSpaceObject>;
|
||||||
|
let fixture: ComponentFixture<BitstreamAuthorizationsComponent<any>>;
|
||||||
|
|
||||||
|
const bitstream = Object.assign(new Bitstream(), {
|
||||||
|
sizeBytes: 10000,
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: 'file name',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
content: { href: 'file-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bitstreamRD = createSuccessfulRemoteDataObject(bitstream);
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
bitstream: bitstreamRD
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [BitstreamAuthorizationsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
|
ChangeDetectorRef,
|
||||||
|
BitstreamAuthorizationsComponent,
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamAuthorizationsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init dso remote data properly', (done) => {
|
||||||
|
const expected = cold('(a|)', { a: bitstreamRD });
|
||||||
|
expect(comp.dsoRD$).toBeObservable(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-authorizations',
|
||||||
|
templateUrl: './bitstream-authorizations.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that handles the Collection Authorizations
|
||||||
|
*/
|
||||||
|
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial DSO object
|
||||||
|
*/
|
||||||
|
public dsoRD$: Observable<RemoteData<TDomain>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {ActivatedRoute} route
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component, setting up the collection
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.bitstream));
|
||||||
|
}
|
||||||
|
}
|
@@ -4,8 +4,14 @@ import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream
|
|||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
||||||
|
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||||
|
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
|
||||||
|
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
|
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
|
|
||||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||||
|
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module to help navigate Bitstream pages
|
* Routing module to help navigate Bitstream pages
|
||||||
@@ -27,6 +33,36 @@ const EDIT_BITSTREAM_PATH = ':id/edit';
|
|||||||
bitstream: BitstreamPageResolver
|
bitstream: BitstreamPageResolver
|
||||||
},
|
},
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH,
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicyTarget: ResourcePolicyTargetResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyCreateComponent,
|
||||||
|
data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicy: ResourcePolicyResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyEditComponent,
|
||||||
|
data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
resolve: {
|
||||||
|
bitstream: BitstreamPageResolver
|
||||||
|
},
|
||||||
|
component: BitstreamAuthorizationsComponent,
|
||||||
|
data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||||
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module handles all components that are necessary for Bitstream related pages
|
* This module handles all components that are necessary for Bitstream related pages
|
||||||
@@ -14,6 +15,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
|||||||
BitstreamPageRoutingModule
|
BitstreamPageRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
BitstreamAuthorizationsComponent,
|
||||||
EditBitstreamPageComponent
|
EditBitstreamPageComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -35,7 +35,7 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
|
|||||||
*/
|
*/
|
||||||
get followLinks(): FollowLinkConfig<Bitstream>[] {
|
get followLinks(): FollowLinkConfig<Bitstream>[] {
|
||||||
return [
|
return [
|
||||||
followLink('bundle', undefined, true, true, true, followLink('item')),
|
followLink('bundle', {}, followLink('item')),
|
||||||
followLink('format')
|
followLink('format')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,11 @@
|
|||||||
[submitLabel]="'form.save'"
|
[submitLabel]="'form.save'"
|
||||||
(submitForm)="onSubmit()"
|
(submitForm)="onSubmit()"
|
||||||
(cancel)="onCancel()"
|
(cancel)="onCancel()"
|
||||||
(dfChange)="onChange($event)"></ds-form>
|
(dfChange)="onChange($event)">
|
||||||
|
<div additional class="container py-3">
|
||||||
|
<a [routerLink]="['/bitstreams', bitstreamRD?.payload?.id, 'authorizations']">{{'bitstream.edit.authorizations.link' | translate}}</a>
|
||||||
|
</div>
|
||||||
|
</ds-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||||
|
@@ -18,12 +18,8 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import {
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
createSuccessfulRemoteDataObject,
|
import { getEntityEditRoute } from '../../+item-page/item-page-routing-paths';
|
||||||
createSuccessfulRemoteDataObject$
|
|
||||||
} from '../../shared/remote-data.utils';
|
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
|
||||||
import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
|
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
@@ -39,7 +35,6 @@ let bitstream: Bitstream;
|
|||||||
let selectedFormat: BitstreamFormat;
|
let selectedFormat: BitstreamFormat;
|
||||||
let allFormats: BitstreamFormat[];
|
let allFormats: BitstreamFormat[];
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let routerStub;
|
|
||||||
|
|
||||||
describe('EditBitstreamPageComponent', () => {
|
describe('EditBitstreamPageComponent', () => {
|
||||||
let comp: EditBitstreamPageComponent;
|
let comp: EditBitstreamPageComponent;
|
||||||
@@ -129,10 +124,6 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemPageUrl = `fake-url/some-uuid`;
|
|
||||||
routerStub = Object.assign(new RouterStub(), {
|
|
||||||
url: `${itemPageUrl}`
|
|
||||||
});
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||||
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
|
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
|
||||||
@@ -142,7 +133,6 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamService },
|
{ provide: BitstreamDataService, useValue: bitstreamService },
|
||||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||||
{ provide: Router, useValue: routerStub },
|
|
||||||
ChangeDetectorRef
|
ChangeDetectorRef
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -154,7 +144,8 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
fixture = TestBed.createComponent(EditBitstreamPageComponent);
|
fixture = TestBed.createComponent(EditBitstreamPageComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
router = (comp as any).router;
|
router = TestBed.inject(Router);
|
||||||
|
spyOn(router, 'navigate');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on startup', () => {
|
describe('on startup', () => {
|
||||||
@@ -241,14 +232,14 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
|
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
|
||||||
comp.itemId = 'some-uuid1';
|
comp.itemId = 'some-uuid1';
|
||||||
comp.navigateToItemEditBitstreams();
|
comp.navigateToItemEditBitstreams();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
|
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
|
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
|
||||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
|
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
|
||||||
comp.itemId = undefined;
|
comp.itemId = undefined;
|
||||||
comp.navigateToItemEditBitstreams();
|
comp.navigateToItemEditBitstreams();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
|
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,10 +19,10 @@ import { cloneDeep } from 'lodash';
|
|||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstCompletedRemoteData,
|
||||||
getRemoteDataPayload,
|
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||||
@@ -131,15 +131,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
rows: 10
|
rows: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The Dynamic Input Model for the file's embargo (disabled on this page)
|
|
||||||
*/
|
|
||||||
embargoModel = new DynamicInputModel({
|
|
||||||
id: 'embargo',
|
|
||||||
name: 'embargo',
|
|
||||||
disabled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the selected format
|
* The Dynamic Input Model for the selected format
|
||||||
*/
|
*/
|
||||||
@@ -159,7 +150,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* All input models in a simple array for easier iterations
|
* All input models in a simple array for easier iterations
|
||||||
*/
|
*/
|
||||||
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel, this.selectedFormatModel, this.newFormatModel];
|
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dynamic form fields used for editing the information of a bitstream
|
* The dynamic form fields used for editing the information of a bitstream
|
||||||
@@ -179,12 +170,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
this.descriptionModel
|
this.descriptionModel
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
new DynamicFormGroupModel({
|
|
||||||
id: 'embargoContainer',
|
|
||||||
group: [
|
|
||||||
this.embargoModel
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new DynamicFormGroupModel({
|
new DynamicFormGroupModel({
|
||||||
id: 'formatContainer',
|
id: 'formatContainer',
|
||||||
group: [
|
group: [
|
||||||
@@ -243,11 +228,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
host: 'row'
|
host: 'row'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
embargoContainer: {
|
|
||||||
grid: {
|
|
||||||
host: 'row'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatContainer: {
|
formatContainer: {
|
||||||
grid: {
|
grid: {
|
||||||
host: 'row'
|
host: 'row'
|
||||||
|
@@ -14,7 +14,7 @@ import { ResolvedAction } from '../core/resolving/resolver.actions';
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
followLink('parentCommunity', {},
|
||||||
followLink('parentCommunity')
|
followLink('parentCommunity')
|
||||||
),
|
),
|
||||||
followLink('logo')
|
followLink('logo')
|
||||||
|
@@ -6,11 +6,12 @@
|
|||||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
|
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
|
||||||
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,11 +6,12 @@
|
|||||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span>
|
||||||
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -4,10 +4,9 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -15,7 +14,6 @@ import { LinkService } from '../../../core/cache/builders/link.service';
|
|||||||
import { Bundle } from '../../../core/shared/bundle.model';
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a bundle's bitstream map entry
|
* Interface for a bundle's bitstream map entry
|
||||||
@@ -79,7 +77,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
map((item: Item) => this.linkService.resolveLink(
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
item,
|
item,
|
||||||
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams'))
|
followLink('bundles', {}, followLink('bitstreams'))
|
||||||
))
|
))
|
||||||
) as Observable<Item>;
|
) as Observable<Item>;
|
||||||
|
|
||||||
|
@@ -5,19 +5,16 @@
|
|||||||
<p>{{'item.edit.move.description' | translate}}</p>
|
<p>{{'item.edit.move.description' | translate}}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ds-dso-input-suggestions #f id="search-form"
|
<div class="card mb-3">
|
||||||
[suggestions]="(collectionSearchResults | async)"
|
<div class="card-header">{{'dso-selector.placeholder' | translate: { type: 'collection' } }}</div>
|
||||||
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
<div class="card-body">
|
||||||
[action]="getCurrentUrl()"
|
<ds-authorized-collection-selector [types]="COLLECTIONS"
|
||||||
[name]="'item-move'"
|
[currentDSOId]="selectedCollection ? selectedCollection.id : originalCollection.id"
|
||||||
[(ngModel)]="selectedCollectionName"
|
(onSelect)="selectDso($event)">
|
||||||
(clickSuggestion)="onClick($event)"
|
</ds-authorized-collection-selector>
|
||||||
(typeSuggestion)="resetCollection($event)"
|
</div>
|
||||||
(findSuggestions)="findSuggestions($event)"
|
<div></div>
|
||||||
(click)="f.open()"
|
</div>
|
||||||
ngDefaultControl>
|
|
||||||
</ds-dso-input-suggestions>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -33,16 +30,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
<div class="button-row bottom">
|
||||||
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
<div class="float-right">
|
||||||
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
||||||
{{'item.edit.move.processing' | translate}}
|
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mr-0" [disabled]="!canMove" (click)="moveToCollection()">
|
||||||
|
<span *ngIf="!processing">
|
||||||
|
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="processing">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()">
|
||||||
class="btn btn-outline-secondary">
|
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
|
||||||
{{'item.edit.move.cancel' | translate}}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -21,6 +21,8 @@ import {
|
|||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
describe('ItemMoveComponent', () => {
|
describe('ItemMoveComponent', () => {
|
||||||
let comp: ItemMoveComponent;
|
let comp: ItemMoveComponent;
|
||||||
@@ -47,18 +49,25 @@ describe('ItemMoveComponent', () => {
|
|||||||
name: 'Test collection 2'
|
name: 'Test collection 2'
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataService = jasmine.createSpyObj({
|
let itemDataService;
|
||||||
moveToCollection: createSuccessfulRemoteDataObject$(collection1)
|
|
||||||
|
const mockItemDataServiceSuccess = jasmine.createSpyObj({
|
||||||
|
moveToCollection: createSuccessfulRemoteDataObject$(collection1),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataServiceFail = jasmine.createSpyObj({
|
const mockItemDataServiceFail = jasmine.createSpyObj({
|
||||||
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500)
|
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
|
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
|
||||||
id: 'item1'
|
id: 'item1',
|
||||||
|
owningCollection: createSuccessfulRemoteDataObject$(Object.assign(new Collection(), {
|
||||||
|
id: 'originalOwningCollection',
|
||||||
|
}))
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -79,8 +88,9 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
const notificationsServiceStub = new NotificationsServiceStub();
|
const notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
describe('ItemMoveComponent success', () => {
|
const init = (mockItemDataService) => {
|
||||||
beforeEach(() => {
|
itemDataService = mockItemDataService;
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [ItemMoveComponent],
|
declarations: [ItemMoveComponent],
|
||||||
@@ -90,6 +100,7 @@ describe('ItemMoveComponent', () => {
|
|||||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
{ provide: SearchService, useValue: mockSearchService },
|
{ provide: SearchService, useValue: mockSearchService },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -97,25 +108,20 @@ describe('ItemMoveComponent', () => {
|
|||||||
fixture = TestBed.createComponent(ItemMoveComponent);
|
fixture = TestBed.createComponent(ItemMoveComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
};
|
||||||
it('should load suggestions', () => {
|
|
||||||
const expected = [
|
|
||||||
collection1,
|
|
||||||
collection2
|
|
||||||
];
|
|
||||||
|
|
||||||
comp.collectionSearchResults.subscribe((value) => {
|
describe('ItemMoveComponent success', () => {
|
||||||
expect(value).toEqual(expected);
|
beforeEach(() => {
|
||||||
}
|
init(mockItemDataServiceSuccess);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get current url ', () => {
|
it('should get current url ', () => {
|
||||||
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
||||||
});
|
});
|
||||||
it('should on click select the correct collection name and id', () => {
|
it('should select the correct collection name and id on click', () => {
|
||||||
const data = collection1;
|
const data = collection1;
|
||||||
|
|
||||||
comp.onClick(data);
|
comp.selectDso(data);
|
||||||
|
|
||||||
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
||||||
expect(comp.selectedCollection).toEqual(collection1);
|
expect(comp.selectedCollection).toEqual(collection1);
|
||||||
@@ -128,12 +134,12 @@ describe('ItemMoveComponent', () => {
|
|||||||
});
|
});
|
||||||
comp.selectedCollectionName = 'selected-collection-id';
|
comp.selectedCollectionName = 'selected-collection-id';
|
||||||
comp.selectedCollection = collection1;
|
comp.selectedCollection = collection1;
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
||||||
});
|
});
|
||||||
it('should call notificationsService success message on success', () => {
|
it('should call notificationsService success message on success', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -142,26 +148,11 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
describe('ItemMoveComponent fail', () => {
|
describe('ItemMoveComponent fail', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
init(mockItemDataServiceFail);
|
||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
|
||||||
declarations: [ItemMoveComponent],
|
|
||||||
providers: [
|
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
|
||||||
{ provide: Router, useValue: routerStub },
|
|
||||||
{ provide: ItemDataService, useValue: mockItemDataServiceFail },
|
|
||||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
|
||||||
{ provide: SearchService, useValue: mockSearchService },
|
|
||||||
], schemas: [
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
fixture = TestBed.createComponent(ItemMoveComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call notificationsService error message on fail', () => {
|
it('should call notificationsService error message on fail', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@@ -1,25 +1,21 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
|
||||||
getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
|
||||||
import { SearchResult } from '../../../shared/search/search-result.model';
|
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-move',
|
selector: 'ds-item-move',
|
||||||
@@ -38,7 +34,8 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
|
|
||||||
inheritPolicies = false;
|
inheritPolicies = false;
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
collectionSearchResults: Observable<any[]> = observableOf([]);
|
originalCollection: Collection;
|
||||||
|
|
||||||
selectedCollectionName: string;
|
selectedCollectionName: string;
|
||||||
selectedCollection: Collection;
|
selectedCollection: Collection;
|
||||||
canSubmit = false;
|
canSubmit = false;
|
||||||
@@ -46,23 +43,26 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
item: Item;
|
item: Item;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
|
||||||
pagination = new PaginationComponentOptions();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item's page
|
* Route to the item's page
|
||||||
*/
|
*/
|
||||||
itemPageRoute$: Observable<string>;
|
itemPageRoute$: Observable<string>;
|
||||||
|
|
||||||
|
COLLECTIONS = [DSpaceObjectType.COLLECTION];
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private itemDataService: ItemDataService,
|
private itemDataService: ItemDataService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private translateService: TranslateService) {
|
private translateService: TranslateService,
|
||||||
}
|
private requestService: RequestService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.dso), getFirstSucceededRemoteData()
|
||||||
|
) as Observable<RemoteData<Item>>;
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item))
|
map((item) => getItemPageRoute(item))
|
||||||
@@ -71,43 +71,22 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.pagination.pageSize = 5;
|
this.itemRD$.pipe(
|
||||||
this.loadSuggestions('');
|
getFirstSucceededRemoteData(),
|
||||||
}
|
getRemoteDataPayload(),
|
||||||
|
switchMap((item) => item.owningCollection),
|
||||||
/**
|
getFirstSucceededRemoteData(),
|
||||||
* Find suggestions based on entered query
|
getRemoteDataPayload(),
|
||||||
* @param query - Search query
|
).subscribe((collection) => {
|
||||||
*/
|
this.originalCollection = collection;
|
||||||
findSuggestions(query): void {
|
|
||||||
this.loadSuggestions(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all available collections to move the item to.
|
|
||||||
* TODO: When the API support it, only fetch collections where user has ADD rights to.
|
|
||||||
*/
|
|
||||||
loadSuggestions(query): void {
|
|
||||||
this.collectionSearchResults = this.searchService.search(new PaginatedSearchOptions({
|
|
||||||
pagination: this.pagination,
|
|
||||||
dsoTypes: [DSpaceObjectType.COLLECTION],
|
|
||||||
query: query
|
|
||||||
})).pipe(
|
|
||||||
first(),
|
|
||||||
map((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
|
|
||||||
return rd.payload.page.map((searchResult) => {
|
|
||||||
return searchResult.indexableObject;
|
|
||||||
});
|
});
|
||||||
}) ,
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the collection name and id based on the selected value
|
* Set the collection name and id based on the selected value
|
||||||
* @param data - obtained from the ds-input-suggestions component
|
* @param data - obtained from the ds-input-suggestions component
|
||||||
*/
|
*/
|
||||||
onClick(data: any): void {
|
selectDso(data: any): void {
|
||||||
this.selectedCollection = data;
|
this.selectedCollection = data;
|
||||||
this.selectedCollectionName = data.name;
|
this.selectedCollectionName = data.name;
|
||||||
this.canSubmit = true;
|
this.canSubmit = true;
|
||||||
@@ -123,26 +102,41 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Moves the item to a new collection based on the selected collection
|
* Moves the item to a new collection based on the selected collection
|
||||||
*/
|
*/
|
||||||
moveCollection() {
|
moveToCollection() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
|
const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection)
|
||||||
(response: RemoteData<Collection>) => {
|
.pipe(getFirstCompletedRemoteData());
|
||||||
this.router.navigate([getItemEditRoute(this.item)]);
|
|
||||||
|
move$.subscribe((response: RemoteData<any>) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
move$.pipe(
|
||||||
|
switchMap(() => this.requestService.setStaleByHrefSubstring(this.item.id)),
|
||||||
|
switchMap(() =>
|
||||||
|
this.itemDataService.findById(
|
||||||
|
this.item.id,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('owningCollection')
|
||||||
|
)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe(() => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
}
|
this.router.navigate([getItemEditRoute(this.item)]);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
discard(): void {
|
||||||
* Resets the can submit when the user changes the content of the input field
|
this.selectedCollection = null;
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
resetCollection(data: any) {
|
|
||||||
this.canSubmit = false;
|
this.canSubmit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canMove(): boolean {
|
||||||
|
return this.canSubmit && this.selectedCollection?.id !== this.originalCollection.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<ng-container *ngVar="updates$ | async as updates">
|
<ng-container *ngVar="updates$ | async as updates">
|
||||||
<ng-container *ngIf="updates">
|
<ng-container *ngIf="updates && !(loading$ | async)">
|
||||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
|
<ds-pagination
|
||||||
|
[paginationOptions]="paginationConfig"
|
||||||
|
[pageInfoState]="(relationshipsRd$ | async)?.payload?.pageInfo"
|
||||||
|
[collectionSize]="(relationshipsRd$ | async)?.payload?.totalElements + (this.nbAddedFields$ | async)"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
<div class="my-2">
|
||||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
class="relationship-row d-block alert"
|
class="relationship-row d-block alert"
|
||||||
[fieldUpdate]="updateValue || {}"
|
[fieldUpdate]="updateValue || {}"
|
||||||
@@ -19,8 +26,10 @@
|
|||||||
'alert-danger': updateValue.changeType === 2
|
'alert-danger': updateValue.changeType === 2
|
||||||
}">
|
}">
|
||||||
</ds-edit-relationship>
|
</ds-edit-relationship>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-loading *ngIf="!updates"></ds-loading>
|
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -16,6 +16,12 @@ import { SharedModule } from '../../../../shared/shared.module';
|
|||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||||
|
import { HostWindowService } from '../../../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
||||||
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
let comp: EditRelationshipListComponent;
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
@@ -25,6 +31,8 @@ let linkService;
|
|||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let relationshipService;
|
let relationshipService;
|
||||||
let selectableListService;
|
let selectableListService;
|
||||||
|
let paginationService;
|
||||||
|
let hostWindowService;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
@@ -37,9 +45,21 @@ let fieldUpdate1;
|
|||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
let relationships;
|
let relationships;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
let paginationOptions;
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
|
||||||
|
const resetComponent = () => {
|
||||||
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
comp.item = item;
|
||||||
|
comp.itemType = entityType;
|
||||||
|
comp.url = url;
|
||||||
|
comp.relationshipType = relationshipType;
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
entityType = Object.assign(new ItemType(), {
|
entityType = Object.assign(new ItemType(), {
|
||||||
@@ -63,6 +83,12 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: `er${relationshipType.id}`,
|
||||||
|
pageSize: 5,
|
||||||
|
currentPage: 1,
|
||||||
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: 'author1',
|
||||||
uuid: 'author1'
|
uuid: 'author1'
|
||||||
@@ -141,6 +167,10 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
resolveLinks: () => null,
|
resolveLinks: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
paginationService = new PaginationServiceStub(paginationOptions);
|
||||||
|
|
||||||
|
hostWindowService = new HostWindowServiceStub(1200);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedModule, TranslateModule.forRoot()],
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
declarations: [EditRelationshipListComponent],
|
declarations: [EditRelationshipListComponent],
|
||||||
@@ -149,22 +179,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
{ provide: RelationshipService, useValue: relationshipService },
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
{ provide: SelectableListService, useValue: selectableListService },
|
{ provide: SelectableListService, useValue: selectableListService },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
|
{ provide: HostWindowService, useValue: hostWindowService },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
resetComponent();
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
}));
|
||||||
comp = fixture.componentInstance;
|
|
||||||
de = fixture.debugElement;
|
|
||||||
comp.item = item;
|
|
||||||
comp.itemType = entityType;
|
|
||||||
comp.url = url;
|
|
||||||
comp.relationshipType = relationshipType;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -176,4 +199,82 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
expect(element.classList).toContain('alert-danger');
|
expect(element.classList).toContain('alert-danger');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pagination component', () => {
|
||||||
|
let paginationComp: PaginationComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive the correct pagination config', () => {
|
||||||
|
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive correct collection size', () => {
|
||||||
|
expect(paginationComp.collectionSize).toEqual(relationships.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('relationshipService.getItemRelationshipsByLabel', () => {
|
||||||
|
it('should receive the correct pagination info', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const findListOptions = callArgs[2];
|
||||||
|
|
||||||
|
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
|
||||||
|
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the left side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(entityType), // publication
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
||||||
|
leftwardType: 'isAuthorOfPublication',
|
||||||
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
});
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the right side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(entityType), // publication
|
||||||
|
leftwardType: 'isPublicationOfAuthor',
|
||||||
|
rightwardType: 'isAuthorOfPublication',
|
||||||
|
});
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of } from 'rxjs';
|
import {
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
from as observableFrom
|
||||||
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
FieldUpdate,
|
FieldUpdate,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
@@ -11,14 +16,24 @@ import {
|
|||||||
} from '../../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { defaultIfEmpty, map, mergeMap, switchMap, take, startWith } from 'rxjs/operators';
|
import {
|
||||||
import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
|
defaultIfEmpty,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
startWith,
|
||||||
|
toArray,
|
||||||
|
tap
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
import { hasValue, hasValueOperator, hasNoValue } from '../../../../shared/empty.util';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getAllSucceededRemoteData,
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
@@ -30,6 +45,10 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
|||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { Collection } from '../../../../core/shared/collection.model';
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -40,7 +59,7 @@ import { Collection } from '../../../../core/shared/collection.model';
|
|||||||
* A component creating a list of editable relationships of a certain type
|
* A component creating a list of editable relationships of a certain type
|
||||||
* The relationships are rendered as a list of related items
|
* The relationships are rendered as a list of related items
|
||||||
*/
|
*/
|
||||||
export class EditRelationshipListComponent implements OnInit {
|
export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item to display related items for
|
* The item to display related items for
|
||||||
@@ -60,6 +79,17 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() relationshipType: RelationshipType;
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the left and right item type of {@link relationshipType} simultaneously.
|
||||||
|
*/
|
||||||
|
private relationshipLeftAndRightType$: Observable<[ItemType, ItemType]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType},
|
||||||
|
* false if it is on the right-hand side and undefined in the rare case that it is on neither side.
|
||||||
|
*/
|
||||||
|
private currentItemIsLeftItem$: Observable<boolean>;
|
||||||
|
|
||||||
private relatedEntityType$: Observable<ItemType>;
|
private relatedEntityType$: Observable<ItemType>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +100,38 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* The FieldUpdates for the relationships in question
|
* The FieldUpdates for the relationships in question
|
||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: BehaviorSubject<FieldUpdates> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RemoteData for the relationships
|
||||||
|
*/
|
||||||
|
relationshipsRd$: BehaviorSubject<RemoteData<PaginatedList<Relationship>>> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current page is the last page
|
||||||
|
*/
|
||||||
|
isLastPage$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we're loading
|
||||||
|
*/
|
||||||
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of added fields that haven't been saved yet
|
||||||
|
*/
|
||||||
|
nbAddedFields$: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination config
|
||||||
|
*/
|
||||||
|
paginationConfig: PaginationComponentOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to the lookup window
|
* A reference to the lookup window
|
||||||
@@ -82,6 +143,7 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected relationshipService: RelationshipService,
|
protected relationshipService: RelationshipService,
|
||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
protected selectableListService: SelectableListService,
|
protected selectableListService: SelectableListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -172,6 +234,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading$.next(true);
|
||||||
|
// emit the last page again to trigger a fieldupdates refresh
|
||||||
|
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -186,6 +252,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.loading$.next(true);
|
||||||
|
// emit the last page again to trigger a fieldupdates refresh
|
||||||
|
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||||
};
|
};
|
||||||
this.relatedEntityType$
|
this.relatedEntityType$
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
@@ -212,10 +282,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
if (field.relationship) {
|
if (field.relationship) {
|
||||||
return this.getRelatedItem(field.relationship);
|
return this.getRelatedItem(field.relationship);
|
||||||
} else {
|
} else {
|
||||||
return of(field.relatedItem);
|
return observableOf(field.relatedItem);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
) : of([])
|
) : observableOf([])
|
||||||
),
|
),
|
||||||
take(1),
|
take(1),
|
||||||
map((items) => items.map((item) => {
|
map((items) => items.map((item) => {
|
||||||
@@ -267,15 +337,16 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// store the left and right type of the relationship in a single observable
|
||||||
this.relatedEntityType$ =
|
this.relationshipLeftAndRightType$ = observableCombineLatest([
|
||||||
observableCombineLatest([
|
|
||||||
this.relationshipType.leftType,
|
this.relationshipType.leftType,
|
||||||
this.relationshipType.rightType,
|
this.relationshipType.rightType,
|
||||||
].map((type) => type.pipe(
|
].map((type) => type.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
))).pipe(
|
))) as Observable<[ItemType, ItemType]>;
|
||||||
|
|
||||||
|
this.relatedEntityType$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
||||||
hasValueOperator()
|
hasValueOperator()
|
||||||
);
|
);
|
||||||
@@ -286,65 +357,142 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updates$ = this.getItemRelationships().pipe(
|
this.currentItemIsLeftItem$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
switchMap((relationships) =>
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
observableCombineLatest(
|
if (leftType.id === this.itemType.id) {
|
||||||
relationships.map((relationship) => this.relationshipService.isLeftItem(relationship, this.item))
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightType.id === this.itemType.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should never happen...
|
||||||
|
console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize the pagination options
|
||||||
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
|
this.paginationConfig.id = `er${this.relationshipType.id}`;
|
||||||
|
this.paginationConfig.pageSize = 5;
|
||||||
|
this.paginationConfig.currentPage = 1;
|
||||||
|
|
||||||
|
// get the pagination params from the route
|
||||||
|
const currentPagination$ = this.paginationService.getCurrentPagination(
|
||||||
|
this.paginationConfig.id,
|
||||||
|
this.paginationConfig
|
||||||
).pipe(
|
).pipe(
|
||||||
defaultIfEmpty([]),
|
tap(() => this.loading$.next(true))
|
||||||
map((isLeftItemArray) => isLeftItemArray.map((isLeftItem, index) => {
|
);
|
||||||
const relationship = relationships[index];
|
|
||||||
const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
currentPagination$,
|
||||||
|
this.currentItemIsLeftItem$,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) =>
|
||||||
|
// get the relationships for the current item, relationshiptype and page
|
||||||
|
this.relationshipService.getItemRelationshipsByLabel(
|
||||||
|
this.item,
|
||||||
|
currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType,
|
||||||
|
{
|
||||||
|
elementsPerPage: currentPagination.pageSize,
|
||||||
|
currentPage: currentPagination.currentPage,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('leftItem'),
|
||||||
|
followLink('rightItem'),
|
||||||
|
)),
|
||||||
|
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
|
this.relationshipsRd$.next(rd);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// keep isLastPage$ up to date based on relationshipsRd$
|
||||||
|
this.subs.push(this.relationshipsRd$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
getAllSucceededRemoteData()
|
||||||
|
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
|
this.isLastPage$.next(hasNoValue(rd.payload._links.next));
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.subs.push(this.relationshipsRd$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<PaginatedList<Relationship>>) =>
|
||||||
|
// emit each relationship in the page separately
|
||||||
|
observableFrom(rd.payload.page).pipe(
|
||||||
|
mergeMap((relationship: Relationship) =>
|
||||||
|
// check for each relationship whether it's the left item
|
||||||
|
this.relationshipService.isLeftItem(relationship, this.item).pipe(
|
||||||
|
// emit an array containing both the relationship and whether it's the left item,
|
||||||
|
// as we'll need both
|
||||||
|
map((isLeftItem: boolean) => [relationship, isLeftItem])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
map(([relationship, isLeftItem]: [Relationship, boolean]) => {
|
||||||
|
// turn it into a RelationshipIdentifiable, an
|
||||||
|
const nameVariant =
|
||||||
|
isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
||||||
return {
|
return {
|
||||||
uuid: relationship.id,
|
uuid: relationship.id,
|
||||||
type: this.relationshipType,
|
type: this.relationshipType,
|
||||||
relationship,
|
relationship,
|
||||||
nameVariant,
|
nameVariant,
|
||||||
} as RelationshipIdentifiable;
|
} as RelationshipIdentifiable;
|
||||||
})),
|
}),
|
||||||
|
// wait until all relationships have been processed, and emit them all as a single array
|
||||||
|
toArray(),
|
||||||
|
// if the pipe above completes without emitting anything, emit an empty array instead
|
||||||
|
defaultIfEmpty([])
|
||||||
)),
|
)),
|
||||||
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields).pipe(
|
switchMap((nextFields: RelationshipIdentifiable[]) => {
|
||||||
map((fieldUpdates) => {
|
// Get a list that contains the unsaved changes for the page, as well as the page of
|
||||||
|
// RelationshipIdentifiables, as a single list of FieldUpdates
|
||||||
|
return this.objectUpdatesService.getFieldUpdates(this.url, nextFields).pipe(
|
||||||
|
map((fieldUpdates: FieldUpdates) => {
|
||||||
const fieldUpdatesFiltered: FieldUpdates = {};
|
const fieldUpdatesFiltered: FieldUpdates = {};
|
||||||
|
this.nbAddedFields$.next(0);
|
||||||
|
// iterate over the fieldupdates and filter out the ones that pertain to this
|
||||||
|
// relationshiptype
|
||||||
Object.keys(fieldUpdates).forEach((uuid) => {
|
Object.keys(fieldUpdates).forEach((uuid) => {
|
||||||
if (hasValue(fieldUpdates[uuid])) {
|
if (hasValue(fieldUpdates[uuid])) {
|
||||||
const field = fieldUpdates[uuid].field;
|
const field = fieldUpdates[uuid].field as RelationshipIdentifiable;
|
||||||
if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) {
|
// only include fieldupdates regarding this RelationshipType
|
||||||
|
if (field.type.id === this.relationshipType.id) {
|
||||||
|
// if it's a newly added relationship
|
||||||
|
if (fieldUpdates[uuid].changeType === FieldChangeType.ADD) {
|
||||||
|
// increase the counter that tracks new relationships
|
||||||
|
this.nbAddedFields$.next(this.nbAddedFields$.getValue() + 1);
|
||||||
|
if (this.isLastPage$.getValue() === true) {
|
||||||
|
// only include newly added relationships to the output if we're on the last
|
||||||
|
// page
|
||||||
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// include all others
|
||||||
|
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return fieldUpdatesFiltered;
|
return fieldUpdatesFiltered;
|
||||||
}),
|
}),
|
||||||
)),
|
|
||||||
startWith({}),
|
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
startWith({}),
|
||||||
|
).subscribe((updates: FieldUpdates) => {
|
||||||
|
this.loading$.next(false);
|
||||||
|
this.updates$.next(updates);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getItemRelationships() {
|
ngOnDestroy(): void {
|
||||||
this.linkService.resolveLink(this.item,
|
this.subs
|
||||||
followLink('relationships', undefined, true, true, true,
|
.filter((subscription) => hasValue(subscription))
|
||||||
followLink('relationshipType'),
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
followLink('leftItem'),
|
|
||||||
followLink('rightItem'),
|
|
||||||
));
|
|
||||||
return this.item.relationships.pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
map((relationships: RemoteData<PaginatedList<Relationship>>) => relationships.payload.page.filter((relationship: Relationship) => hasValue(relationship))),
|
|
||||||
switchMap((itemRelationships: Relationship[]) =>
|
|
||||||
observableCombineLatest(
|
|
||||||
itemRelationships
|
|
||||||
.map((relationship) => relationship.relationshipType.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
))
|
|
||||||
).pipe(
|
|
||||||
defaultIfEmpty([]),
|
|
||||||
map((relationshipTypes) => itemRelationships.filter(
|
|
||||||
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -227,7 +227,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
console.log('init');
|
|
||||||
return this.relationshipService.getRelatedItems(this.item).pipe(
|
return this.relationshipService.getRelatedItems(this.item).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe((items: Item[]) => {
|
).subscribe((items: Item[]) => {
|
||||||
|
@@ -68,7 +68,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
followLink('format')
|
followLink('format'),
|
||||||
|
followLink('thumbnail'),
|
||||||
)),
|
)),
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(rd.errorMessage)) {
|
if (hasValue(rd.errorMessage)) {
|
||||||
@@ -85,7 +86,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
followLink('format')
|
followLink('format'),
|
||||||
|
followLink('thumbnail'),
|
||||||
)),
|
)),
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(rd.errorMessage)) {
|
if (hasValue(rd.errorMessage)) {
|
||||||
|
@@ -5,7 +5,6 @@ import { RemoteData } from '../core/data/remote-data';
|
|||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
||||||
@@ -15,13 +14,13 @@ import { ResolvedAction } from '../core/resolving/resolver.actions';
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
||||||
followLink('owningCollection', undefined, true, true, true,
|
followLink('owningCollection', {},
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
followLink('parentCommunity', {},
|
||||||
followLink('parentCommunity'))
|
followLink('parentCommunity'))
|
||||||
),
|
),
|
||||||
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
|
|
||||||
followLink('relationships'),
|
followLink('relationships'),
|
||||||
followLink('version', undefined, true, true, true, followLink('versionhistory')),
|
followLink('version', {}, followLink('versionhistory')),
|
||||||
|
followLink('thumbnail')
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ng-container *ngIf="!mediaViewer.image">
|
<ng-container *ngIf="!mediaViewer.image">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="mediaViewer.image">
|
<ng-container *ngIf="mediaViewer.image">
|
||||||
|
@@ -1,12 +1,7 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { takeUntilCompletedRemoteData } from '../../../../core/shared/operators';
|
|
||||||
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
||||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item',
|
selector: 'ds-item',
|
||||||
@@ -18,28 +13,14 @@ import { RemoteData } from '../../../../core/data/remote-data';
|
|||||||
export class ItemComponent implements OnInit {
|
export class ItemComponent implements OnInit {
|
||||||
@Input() object: Item;
|
@Input() object: Item;
|
||||||
|
|
||||||
/**
|
|
||||||
* The Item's thumbnail
|
|
||||||
*/
|
|
||||||
thumbnail$: BehaviorSubject<RemoteData<Bitstream>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item page
|
* Route to the item page
|
||||||
*/
|
*/
|
||||||
itemPageRoute: string;
|
itemPageRoute: string;
|
||||||
mediaViewer = environment.mediaViewer;
|
|
||||||
|
|
||||||
constructor(protected bitstreamDataService: BitstreamDataService) {
|
mediaViewer = environment.mediaViewer;
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemPageRoute = getItemPageRoute(this.object);
|
this.itemPageRoute = getItemPageRoute(this.object);
|
||||||
|
|
||||||
this.thumbnail$ = new BehaviorSubject<RemoteData<Bitstream>>(undefined);
|
|
||||||
this.bitstreamDataService.getThumbnailFor(this.object).pipe(
|
|
||||||
takeUntilCompletedRemoteData(),
|
|
||||||
).subscribe((rd: RemoteData<Bitstream>) => {
|
|
||||||
this.thumbnail$.next(rd);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ng-container *ngIf="!mediaViewer.image">
|
<ng-container *ngIf="!mediaViewer.image">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="mediaViewer.image">
|
<ng-container *ngIf="mediaViewer.image">
|
||||||
|
@@ -19,6 +19,8 @@ import { currentPath } from '../shared/utils/route.utils';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -128,8 +130,11 @@ export class SearchComponent implements OnInit {
|
|||||||
this.searchLink = this.getSearchLink();
|
this.searchLink = this.getSearchLink();
|
||||||
this.searchOptions$ = this.getSearchOptions();
|
this.searchOptions$ = this.getSearchOptions();
|
||||||
this.sub = this.searchOptions$.pipe(
|
this.sub = this.searchOptions$.pipe(
|
||||||
switchMap((options) => this.service.search(options).pipe(getFirstSucceededRemoteData(), startWith(undefined))))
|
switchMap((options) => this.service.search(
|
||||||
.subscribe((results) => {
|
options, undefined, true, true, followLink<Item>('thumbnail', { isOptional: true })
|
||||||
|
).pipe(getFirstSucceededRemoteData(), startWith(undefined))
|
||||||
|
)
|
||||||
|
).subscribe((results) => {
|
||||||
this.resultsRD$.next(results);
|
this.resultsRD$.next(results);
|
||||||
});
|
});
|
||||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
|
@@ -174,8 +174,8 @@ export class CommunityListService {
|
|||||||
direction: options.sort.direction
|
direction: options.sort.direction
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
followLink('subcommunities', this.configOnePage, true, true),
|
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
||||||
followLink('collections', this.configOnePage, true, true))
|
followLink('collections', { findListOptions: this.configOnePage }))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
map((results) => results.payload),
|
map((results) => results.payload),
|
||||||
@@ -242,8 +242,8 @@ export class CommunityListService {
|
|||||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
},
|
},
|
||||||
followLink('subcommunities', this.configOnePage, true, true),
|
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
||||||
followLink('collections', this.configOnePage, true, true))
|
followLink('collections', { findListOptions: this.configOnePage }))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((rd: RemoteData<PaginatedList<Community>>) => {
|
switchMap((rd: RemoteData<PaginatedList<Community>>) => {
|
||||||
|
@@ -292,10 +292,13 @@ export class ResetAuthenticationMessagesAction implements Action {
|
|||||||
export class RetrieveAuthMethodsAction implements Action {
|
export class RetrieveAuthMethodsAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
||||||
|
|
||||||
payload: AuthStatus;
|
payload: {
|
||||||
|
status: AuthStatus;
|
||||||
|
blocking: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(authStatus: AuthStatus) {
|
constructor(status: AuthStatus, blocking: boolean) {
|
||||||
this.payload = authStatus;
|
this.payload = { status, blocking };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,10 +309,14 @@ export class RetrieveAuthMethodsAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class RetrieveAuthMethodsSuccessAction implements Action {
|
export class RetrieveAuthMethodsSuccessAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
||||||
payload: AuthMethod[];
|
|
||||||
|
|
||||||
constructor(authMethods: AuthMethod[] ) {
|
payload: {
|
||||||
this.payload = authMethods;
|
authMethods: AuthMethod[];
|
||||||
|
blocking: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(authMethods: AuthMethod[], blocking: boolean ) {
|
||||||
|
this.payload = { authMethods, blocking };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +327,12 @@ export class RetrieveAuthMethodsSuccessAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class RetrieveAuthMethodsErrorAction implements Action {
|
export class RetrieveAuthMethodsErrorAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
||||||
|
|
||||||
|
payload: boolean;
|
||||||
|
|
||||||
|
constructor(blocking: boolean) {
|
||||||
|
this.payload = blocking;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -43,10 +43,12 @@ describe('AuthEffects', () => {
|
|||||||
let initialState;
|
let initialState;
|
||||||
let token;
|
let token;
|
||||||
let store: MockStore<AppState>;
|
let store: MockStore<AppState>;
|
||||||
|
let authStatus;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
authServiceStub = new AuthServiceStub();
|
authServiceStub = new AuthServiceStub();
|
||||||
token = authServiceStub.getToken();
|
token = authServiceStub.getToken();
|
||||||
|
authStatus = Object.assign(new AuthStatus(), {});
|
||||||
initialState = {
|
initialState = {
|
||||||
core: {
|
core: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -217,19 +219,41 @@ describe('AuthEffects', () => {
|
|||||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on CSR', () => {
|
||||||
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||||
observableOf(
|
observableOf(
|
||||||
{ authenticated: false })
|
{ authenticated: false })
|
||||||
);
|
);
|
||||||
|
spyOn((authEffects as any).authService, 'getRetrieveAuthMethodsAction').and.returnValue(
|
||||||
|
new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, false)
|
||||||
|
);
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus) });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, false) });
|
||||||
|
|
||||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on SSR', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||||
|
observableOf(
|
||||||
|
{ authenticated: false })
|
||||||
|
);
|
||||||
|
spyOn((authEffects as any).authService, 'getRetrieveAuthMethodsAction').and.returnValue(
|
||||||
|
new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, true)
|
||||||
|
);
|
||||||
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, true) });
|
||||||
|
|
||||||
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when check token failed', () => {
|
describe('when check token failed', () => {
|
||||||
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||||
@@ -359,11 +383,17 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
describe('retrieveMethods$', () => {
|
describe('retrieveMethods$', () => {
|
||||||
|
|
||||||
|
describe('on CSR', () => {
|
||||||
describe('when retrieve authentication methods succeeded', () => {
|
describe('when retrieve authentication methods succeeded', () => {
|
||||||
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: false}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock) });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock, false) });
|
||||||
|
|
||||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -373,15 +403,56 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: false}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction() });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction(false) });
|
||||||
|
|
||||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on SSR', () => {
|
||||||
|
describe('when retrieve authentication methods succeeded', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: true}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock, true) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when retrieve authentication methods failed', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: true}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction(true) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('clearInvalidTokenOnRehydrate$', () => {
|
describe('clearInvalidTokenOnRehydrate$', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@@ -145,7 +145,7 @@ export class AuthEffects {
|
|||||||
if (response.authenticated) {
|
if (response.authenticated) {
|
||||||
return new RetrieveTokenAction();
|
return new RetrieveTokenAction();
|
||||||
} else {
|
} else {
|
||||||
return new RetrieveAuthMethodsAction(response);
|
return this.authService.getRetrieveAuthMethodsAction(response);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
||||||
@@ -234,10 +234,10 @@ export class AuthEffects {
|
|||||||
.pipe(
|
.pipe(
|
||||||
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
||||||
switchMap((action: RetrieveAuthMethodsAction) => {
|
switchMap((action: RetrieveAuthMethodsAction) => {
|
||||||
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload)
|
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload.status)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)),
|
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels, action.payload.blocking)),
|
||||||
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction()))
|
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction(action.payload.blocking)))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -512,7 +512,7 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthMethodsAction(new AuthStatus());
|
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
@@ -536,7 +536,7 @@ describe('authReducer', () => {
|
|||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||||
];
|
];
|
||||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
const action = new RetrieveAuthMethodsSuccessAction(authMethods, false);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
@@ -548,6 +548,30 @@ describe('authReducer', () => {
|
|||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_SUCCESS action with blocking as true', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
|
authMethods: []
|
||||||
|
};
|
||||||
|
const authMethods = [
|
||||||
|
new AuthMethod(AuthMethodType.Password),
|
||||||
|
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||||
|
];
|
||||||
|
const action = new RetrieveAuthMethodsSuccessAction(authMethods, true);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: false,
|
||||||
|
authMethods: authMethods
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action ', () => {
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action ', () => {
|
||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
@@ -557,7 +581,7 @@ describe('authReducer', () => {
|
|||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new RetrieveAuthMethodsErrorAction();
|
const action = new RetrieveAuthMethodsErrorAction(false);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
@@ -568,4 +592,25 @@ describe('authReducer', () => {
|
|||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action with blocking as true', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
|
authMethods: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new RetrieveAuthMethodsErrorAction(true);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: false,
|
||||||
|
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
RedirectWhenTokenExpiredAction,
|
RedirectWhenTokenExpiredAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
RetrieveAuthenticatedEpersonSuccessAction,
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
@@ -211,14 +212,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: false,
|
loading: false,
|
||||||
blocking: false,
|
blocking: (action as RetrieveAuthMethodsSuccessAction).payload.blocking,
|
||||||
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload.authMethods
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: false,
|
loading: false,
|
||||||
blocking: false,
|
blocking: (action as RetrieveAuthMethodsErrorAction).payload,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -35,6 +35,7 @@ import { AppState } from '../../app.reducer';
|
|||||||
import {
|
import {
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction,
|
||||||
|
RetrieveAuthMethodsAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
@@ -518,4 +519,13 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new instance of RetrieveAuthMethodsAction
|
||||||
|
*
|
||||||
|
* @param authStatus The auth status
|
||||||
|
*/
|
||||||
|
getRetrieveAuthMethodsAction(authStatus: AuthStatus): RetrieveAuthMethodsAction {
|
||||||
|
return new RetrieveAuthMethodsAction(authStatus, false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import { AuthService } from './auth.service';
|
|||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RetrieveAuthMethodsAction } from './auth.actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth service.
|
* The auth service.
|
||||||
@@ -60,4 +61,13 @@ export class ServerAuthService extends AuthService {
|
|||||||
map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload))
|
map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new instance of RetrieveAuthMethodsAction
|
||||||
|
*
|
||||||
|
* @param authStatus The auth status
|
||||||
|
*/
|
||||||
|
getRetrieveAuthMethodsAction(authStatus: AuthStatus): RetrieveAuthMethodsAction {
|
||||||
|
return new RetrieveAuthMethodsAction(authStatus, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
src/app/core/cache/builders/link.service.spec.ts
vendored
10
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -102,7 +102,7 @@ describe('LinkService', () => {
|
|||||||
describe('resolveLink', () => {
|
describe('resolveLink', () => {
|
||||||
describe(`when the linkdefinition concerns a single object`, () => {
|
describe(`when the linkdefinition concerns a single object`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
||||||
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
|
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
|
||||||
@@ -116,7 +116,7 @@ describe('LinkService', () => {
|
|||||||
propertyName: 'predecessor',
|
propertyName: 'predecessor',
|
||||||
isList: true
|
isList: true
|
||||||
});
|
});
|
||||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
||||||
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
||||||
@@ -124,7 +124,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
describe('either way', () => {
|
describe('either way', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getLinkDefinition with the correct model and link', () => {
|
it('should call getLinkDefinition with the correct model and link', () => {
|
||||||
@@ -149,7 +149,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
7
src/app/core/cache/builders/link.service.ts
vendored
7
src/app/core/cache/builders/link.service.ts
vendored
@@ -55,9 +55,7 @@ export class LinkService {
|
|||||||
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
||||||
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
||||||
|
|
||||||
if (hasNoValue(matchingLinkDef)) {
|
if (hasValue(matchingLinkDef)) {
|
||||||
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
|
||||||
} else {
|
|
||||||
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
||||||
|
|
||||||
if (hasNoValue(provider)) {
|
if (hasNoValue(provider)) {
|
||||||
@@ -84,7 +82,10 @@ export class LinkService {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!linkToFollow.isOptional) {
|
||||||
|
throw new Error(`followLink('${linkToFollow.name}') was used as a required link for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => {
|
|||||||
let paginatedLinksToFollow;
|
let paginatedLinksToFollow;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
paginatedLinksToFollow = [
|
paginatedLinksToFollow = [
|
||||||
followLink('page', undefined, true, true, true, ...linksToFollow),
|
followLink('page', {}, ...linksToFollow),
|
||||||
...linksToFollow
|
...linksToFollow
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@@ -271,7 +271,7 @@ export class RemoteDataBuildService {
|
|||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, true, true, ...linksToFollow));
|
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', { shouldEmbed: false }, ...linksToFollow));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -162,6 +162,7 @@ import { UsageReport } from './statistics/models/usage-report.model';
|
|||||||
import { RootDataService } from './data/root-data.service';
|
import { RootDataService } from './data/root-data.service';
|
||||||
import { Root } from './data/root.model';
|
import { Root } from './data/root.model';
|
||||||
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
||||||
|
import { SequenceService } from './shared/sequence.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -282,7 +283,8 @@ const PROVIDERS = [
|
|||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||||
VocabularyService,
|
VocabularyService,
|
||||||
VocabularyTreeviewService
|
VocabularyTreeviewService,
|
||||||
|
SequenceService,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
@@ -18,7 +18,7 @@ import { Item } from '../shared/item.model';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { PaginatedList, buildPaginatedList } from './paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FindListOptions, PutRequest } from './request.models';
|
import { FindListOptions, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -28,7 +28,6 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
|||||||
import { sendRequest } from '../shared/operators';
|
import { sendRequest } from '../shared/operators';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { RequestEntryState } from './request.reducer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
@@ -75,92 +74,6 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the thumbnail for the given item
|
|
||||||
* @returns {Observable<RemoteData<{@link Bitstream}>>} the first bitstream in the THUMBNAIL bundle
|
|
||||||
*/
|
|
||||||
// TODO should be implemented rest side. {@link Item} should get a thumbnail link
|
|
||||||
public getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
|
||||||
if (isNotEmpty(bundleRD.payload)) {
|
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe(
|
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
bitstreamRD.state,
|
|
||||||
bitstreamRD.errorMessage,
|
|
||||||
bitstreamRD.payload.page[0],
|
|
||||||
bitstreamRD.statusCode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return bitstreamRD as any;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [bundleRD as any];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the matching thumbnail for a {@link Bitstream}.
|
|
||||||
*
|
|
||||||
* The {@link Item} is technically redundant, but is available
|
|
||||||
* in all current use cases, and having it simplifies this method
|
|
||||||
*
|
|
||||||
* @param item The {@link Item} the {@link Bitstream} and its thumbnail are a part of
|
|
||||||
* @param bitstreamInOriginal The original {@link Bitstream} to find the thumbnail for
|
|
||||||
*/
|
|
||||||
// TODO should be implemented rest side
|
|
||||||
public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable<RemoteData<Bitstream>> {
|
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
|
||||||
if (isNotEmpty(bundleRD.payload)) {
|
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 9999 }).pipe(
|
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
|
||||||
const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) =>
|
|
||||||
thumbnail.name.startsWith(bitstreamInOriginal.name)
|
|
||||||
);
|
|
||||||
if (hasValue(matchingThumbnail)) {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
bitstreamRD.state,
|
|
||||||
bitstreamRD.errorMessage,
|
|
||||||
matchingThumbnail,
|
|
||||||
bitstreamRD.statusCode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
RequestEntryState.Error,
|
|
||||||
'No matching thumbnail found',
|
|
||||||
undefined,
|
|
||||||
404
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return bitstreamRD as any;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [bundleRD as any];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all {@link Bitstream}s in a certain {@link Bundle}.
|
* Retrieve all {@link Bitstream}s in a certain {@link Bundle}.
|
||||||
*
|
*
|
||||||
|
@@ -233,7 +233,7 @@ describe('DataService', () => {
|
|||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
elementsPerPage: 5
|
elementsPerPage: 5
|
||||||
});
|
});
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles', config, true, true, true)).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -253,7 +253,7 @@ describe('DataService', () => {
|
|||||||
elementsPerPage: 2
|
elementsPerPage: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', config, true, true, true), followLink('templateItemOf')).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -261,7 +261,13 @@ describe('DataService', () => {
|
|||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${endpoint}?embed=templateItemOf`;
|
const expected = `${endpoint}?embed=templateItemOf`;
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')).subscribe((value) => {
|
(service as any).getFindAllHref(
|
||||||
|
{},
|
||||||
|
null,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -269,7 +275,7 @@ describe('DataService', () => {
|
|||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -279,7 +285,7 @@ describe('DataService', () => {
|
|||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
elementsPerPage: 4
|
elementsPerPage: 4
|
||||||
});
|
});
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', config, true, true, true))).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -308,13 +314,19 @@ describe('DataService', () => {
|
|||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
|
const result = (service as any).getIDHref(
|
||||||
|
endpointMock,
|
||||||
|
resourceIdMock,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, true, true,followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -174,13 +174,29 @@ describe('DsoRedirectDataService', () => {
|
|||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
|
const result = (service as any).getIDHref(
|
||||||
|
pidLink,
|
||||||
|
dsoUUID,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
|
const result = (service as any).getIDHref(
|
||||||
|
pidLink,
|
||||||
|
dsoUUID,
|
||||||
|
followLink('owningCollection',
|
||||||
|
{},
|
||||||
|
followLink('itemtemplate',
|
||||||
|
{},
|
||||||
|
followLink('relationships')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -23,14 +23,7 @@ import { DataService } from './data.service';
|
|||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models';
|
||||||
DeleteRequest,
|
|
||||||
FindListOptions,
|
|
||||||
GetRequest,
|
|
||||||
PostRequest,
|
|
||||||
PutRequest,
|
|
||||||
RestRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
@@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(ITEM)
|
@dataService(ITEM)
|
||||||
@@ -229,7 +225,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
* @param itemId
|
* @param itemId
|
||||||
* @param collection
|
* @param collection
|
||||||
*/
|
*/
|
||||||
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<Collection>> {
|
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<any>> {
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'text/uri-list');
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
@@ -242,9 +238,17 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PutRequest(requestId, href, collection._links.self.href, options);
|
const request = new PutRequest(requestId, href, collection._links.self.href, options);
|
||||||
this.requestService.send(request);
|
Object.assign(request, {
|
||||||
|
// TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return StatusCodeOnlyResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return request;
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe((request) => {
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ export const JsonPatchOperationsActionTypes = {
|
|||||||
ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'),
|
ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'),
|
||||||
FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'),
|
FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'),
|
||||||
START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'),
|
START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'),
|
||||||
|
DELETE_PENDING_JSON_PATCH_OPERATIONS: type('dspace/core/patch/DELETE_PENDING_JSON_PATCH_OPERATIONS'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -261,6 +262,13 @@ export class NewPatchReplaceOperationAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to delete all pending JSON Patch Operations.
|
||||||
|
*/
|
||||||
|
export class DeletePendingJsonPatchOperationsAction implements Action {
|
||||||
|
type = JsonPatchOperationsActionTypes.DELETE_PENDING_JSON_PATCH_OPERATIONS;
|
||||||
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -276,4 +284,5 @@ export type PatchOperationsActions
|
|||||||
| NewPatchRemoveOperationAction
|
| NewPatchRemoveOperationAction
|
||||||
| NewPatchReplaceOperationAction
|
| NewPatchReplaceOperationAction
|
||||||
| RollbacktPatchOperationsAction
|
| RollbacktPatchOperationsAction
|
||||||
| StartTransactionPatchOperationsAction;
|
| StartTransactionPatchOperationsAction
|
||||||
|
| DeletePendingJsonPatchOperationsAction;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommitPatchOperationsAction,
|
CommitPatchOperationsAction, DeletePendingJsonPatchOperationsAction,
|
||||||
FlushPatchOperationsAction,
|
FlushPatchOperationsAction,
|
||||||
NewPatchAddOperationAction,
|
NewPatchAddOperationAction,
|
||||||
NewPatchRemoveOperationAction,
|
NewPatchRemoveOperationAction,
|
||||||
@@ -323,4 +323,19 @@ describe('jsonPatchOperationsReducer test suite', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When DeletePendingJsonPatchOperationsAction has been dispatched', () => {
|
||||||
|
it('should set set the JsonPatchOperationsState to null ', () => {
|
||||||
|
const action = new DeletePendingJsonPatchOperationsAction();
|
||||||
|
initState = Object.assign({}, testState, {
|
||||||
|
[testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
|
||||||
|
transactionStartTime: startTimestamp,
|
||||||
|
commitPending: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const newState = jsonPatchOperationsReducer(initState, action);
|
||||||
|
|
||||||
|
expect(newState).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -11,7 +11,8 @@ import {
|
|||||||
NewPatchReplaceOperationAction,
|
NewPatchReplaceOperationAction,
|
||||||
CommitPatchOperationsAction,
|
CommitPatchOperationsAction,
|
||||||
StartTransactionPatchOperationsAction,
|
StartTransactionPatchOperationsAction,
|
||||||
RollbacktPatchOperationsAction
|
RollbacktPatchOperationsAction,
|
||||||
|
DeletePendingJsonPatchOperationsAction
|
||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
|
import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
|
||||||
|
|
||||||
@@ -101,6 +102,10 @@ export function jsonPatchOperationsReducer(state = initialState, action: PatchOp
|
|||||||
return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
|
return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case JsonPatchOperationsActionTypes.DELETE_PENDING_JSON_PATCH_OPERATIONS: {
|
||||||
|
return deletePendingOperations(state, action as DeletePendingJsonPatchOperationsAction);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -178,6 +183,20 @@ function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the JsonPatchOperationsState to its initial value.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an DeletePendingJsonPatchOperationsAction
|
||||||
|
* @return JsonPatchOperationsState
|
||||||
|
* the new state.
|
||||||
|
*/
|
||||||
|
function deletePendingOperations(state: JsonPatchOperationsState, action: DeletePendingJsonPatchOperationsAction): JsonPatchOperationsState {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new JSON patch operation list.
|
* Add new JSON patch operation list.
|
||||||
*
|
*
|
||||||
|
@@ -17,6 +17,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
|
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
|
||||||
import {
|
import {
|
||||||
CommitPatchOperationsAction,
|
CommitPatchOperationsAction,
|
||||||
|
DeletePendingJsonPatchOperationsAction,
|
||||||
RollbacktPatchOperationsAction,
|
RollbacktPatchOperationsAction,
|
||||||
StartTransactionPatchOperationsAction
|
StartTransactionPatchOperationsAction
|
||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
@@ -288,4 +289,19 @@ describe('JsonPatchOperationsService test suite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deletePendingJsonPatchOperations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.dispatch.and.callFake(() => { /* */ });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch a new DeletePendingJsonPatchOperationsAction', () => {
|
||||||
|
|
||||||
|
const expectedAction = new DeletePendingJsonPatchOperationsAction();
|
||||||
|
scheduler.schedule(() => service.deletePendingJsonPatchOperations());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { jsonPatchOperationsByResourceType } from './selectors';
|
import { jsonPatchOperationsByResourceType } from './selectors';
|
||||||
import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
|
import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
|
||||||
import {
|
import {
|
||||||
CommitPatchOperationsAction,
|
CommitPatchOperationsAction, DeletePendingJsonPatchOperationsAction,
|
||||||
RollbacktPatchOperationsAction,
|
RollbacktPatchOperationsAction,
|
||||||
StartTransactionPatchOperationsAction
|
StartTransactionPatchOperationsAction
|
||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
@@ -105,6 +105,13 @@ export abstract class JsonPatchOperationsService<ResponseDefinitionDomain, Patch
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an action to delete all pending JSON patch Operations.
|
||||||
|
*/
|
||||||
|
public deletePendingJsonPatchOperations() {
|
||||||
|
this.store.dispatch(new DeletePendingJsonPatchOperationsAction());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance for RestRequest class
|
* Return an instance for RestRequest class
|
||||||
*
|
*
|
||||||
|
@@ -43,13 +43,14 @@ export class Bitstream extends DSpaceObject implements HALResource {
|
|||||||
bundle: HALLink;
|
bundle: HALLink;
|
||||||
format: HALLink;
|
format: HALLink;
|
||||||
content: HALLink;
|
content: HALLink;
|
||||||
|
thumbnail: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The thumbnail for this Bitstream
|
* The thumbnail for this Bitstream
|
||||||
* Needs to be resolved first, but isn't available as a {@link HALLink} yet
|
* Will be undefined unless the thumbnail {@link HALLink} has been resolved.
|
||||||
* Use BitstreamDataService.getThumbnailFor(…) for now.
|
|
||||||
*/
|
*/
|
||||||
|
@link(BITSTREAM, false, 'thumbnail')
|
||||||
thumbnail?: Observable<RemoteData<Bitstream>>;
|
thumbnail?: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
|
import { autoserialize, autoserializeAs, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { isEmpty } from '../../shared/empty.util';
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
@@ -19,6 +19,8 @@ import { ITEM } from './item.resource-type';
|
|||||||
import { ChildHALResource } from './child-hal-resource.model';
|
import { ChildHALResource } from './child-hal-resource.model';
|
||||||
import { Version } from './version.model';
|
import { Version } from './version.model';
|
||||||
import { VERSION } from './version.resource-type';
|
import { VERSION } from './version.resource-type';
|
||||||
|
import { BITSTREAM } from './bitstream.resource-type';
|
||||||
|
import { Bitstream } from './bitstream.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a DSpace Item
|
* Class representing a DSpace Item
|
||||||
@@ -37,7 +39,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
|||||||
/**
|
/**
|
||||||
* The Date of the last modification of this Item
|
* The Date of the last modification of this Item
|
||||||
*/
|
*/
|
||||||
@deserialize
|
@deserializeAs(Date)
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +71,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
|||||||
owningCollection: HALLink;
|
owningCollection: HALLink;
|
||||||
templateItemOf: HALLink;
|
templateItemOf: HALLink;
|
||||||
version: HALLink;
|
version: HALLink;
|
||||||
|
thumbnail: HALLink;
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,6 +103,13 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
|||||||
@link(RELATIONSHIP, true)
|
@link(RELATIONSHIP, true)
|
||||||
relationships?: Observable<RemoteData<PaginatedList<Relationship>>>;
|
relationships?: Observable<RemoteData<PaginatedList<Relationship>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The thumbnail for this Item
|
||||||
|
* Will be undefined unless the thumbnail {@link HALLink} has been resolved.
|
||||||
|
*/
|
||||||
|
@link(BITSTREAM, false, 'thumbnail')
|
||||||
|
thumbnail?: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that returns as which type of object this object should be rendered
|
* Method that returns as which type of object this object should be rendered
|
||||||
*/
|
*/
|
||||||
|
@@ -41,6 +41,43 @@ import { SearchConfig } from './search-filters/search-config.model';
|
|||||||
import { PaginationService } from '../../pagination/pagination.service';
|
import { PaginationService } from '../../pagination/pagination.service';
|
||||||
import { SearchConfigurationService } from './search-configuration.service';
|
import { SearchConfigurationService } from './search-configuration.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { DataService } from '../../data/data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DSOChangeAnalyzer } from '../../data/dso-change-analyzer.service';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* A class that lets us delegate some methods to DataService
|
||||||
|
*/
|
||||||
|
class DataServiceImpl extends DataService<any> {
|
||||||
|
protected linkPath = 'discover';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<any>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the embed options to the link for the request
|
||||||
|
* @param href The href the params are to be added to
|
||||||
|
* @param args params for the query string
|
||||||
|
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||||
|
*/
|
||||||
|
public addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<any>[]) {
|
||||||
|
return super.addEmbedParams(href, args, ...linksToFollow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all general actions that have to do with the search page
|
* Service that performs all general actions that have to do with the search page
|
||||||
@@ -78,6 +115,11 @@ export class SearchService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private sub;
|
private sub;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of DataServiceImpl that lets us delegate some methods to DataService
|
||||||
|
*/
|
||||||
|
private searchDataService: DataServiceImpl;
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private routeService: RouteService,
|
private routeService: RouteService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -89,6 +131,16 @@ export class SearchService implements OnDestroy {
|
|||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private searchConfigurationService: SearchConfigurationService
|
private searchConfigurationService: SearchConfigurationService
|
||||||
) {
|
) {
|
||||||
|
this.searchDataService = new DataServiceImpl(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,7 +183,17 @@ export class SearchService implements OnDestroy {
|
|||||||
search<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> {
|
search<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> {
|
||||||
const href$ = this.getEndpoint(searchOptions);
|
const href$ = this.getEndpoint(searchOptions);
|
||||||
|
|
||||||
href$.pipe(take(1)).subscribe((url: string) => {
|
href$.pipe(
|
||||||
|
take(1),
|
||||||
|
map((href: string) => {
|
||||||
|
const args = this.searchDataService.addEmbedParams(href, [], ...linksToFollow);
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
} else {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe((url: string) => {
|
||||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
|
||||||
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||||
|
22
src/app/core/shared/sequence.service.spec.ts
Normal file
22
src/app/core/shared/sequence.service.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SequenceService } from './sequence.service';
|
||||||
|
|
||||||
|
let service: SequenceService;
|
||||||
|
|
||||||
|
describe('SequenceService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new SequenceService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sequential numbers on next(), starting with 1', () => {
|
||||||
|
const NUMBERS = [1,2,3,4,5];
|
||||||
|
const sequence = NUMBERS.map(() => service.next());
|
||||||
|
expect(sequence).toEqual(NUMBERS);
|
||||||
|
});
|
||||||
|
});
|
24
src/app/core/shared/sequence.service.ts
Normal file
24
src/app/core/shared/sequence.service.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* Provides unique sequential numbers
|
||||||
|
*/
|
||||||
|
export class SequenceService {
|
||||||
|
private value: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public next(): number {
|
||||||
|
return ++this.value;
|
||||||
|
}
|
||||||
|
}
|
@@ -173,7 +173,7 @@ export class VocabularyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
||||||
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
|
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
||||||
);
|
);
|
||||||
@@ -200,7 +200,7 @@ export class VocabularyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
||||||
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
|
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
||||||
);
|
);
|
||||||
@@ -249,7 +249,7 @@ export class VocabularyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
|
||||||
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
|
return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
|
||||||
getFirstSucceededRemoteListPayload(),
|
getFirstSucceededRemoteListPayload(),
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['publicationvolume.volumeNumber']"
|
[fields]="['publicationvolume.volumeNumber']"
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['publicationvolume.volumeNumber']"
|
[fields]="['publicationvolume.volumeNumber']"
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||||
[fields]="['creativeworkseries.issn']"
|
[fields]="['creativeworkseries.issn']"
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
||||||
[alt]="'thumbnail.orgunit.alt'"
|
[alt]="'thumbnail.orgunit.alt'"
|
||||||
[placeholder]="'thumbnail.orgunit.placeholder'"
|
[placeholder]="'thumbnail.orgunit.placeholder'"
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/person-placeholder.svg'"
|
[defaultImage]="'assets/images/person-placeholder.svg'"
|
||||||
[alt]="'thumbnail.person.alt'"
|
[alt]="'thumbnail.person.alt'"
|
||||||
[placeholder]="'thumbnail.person.placeholder'">
|
[placeholder]="'thumbnail.person.placeholder'">
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail
|
<ds-thumbnail
|
||||||
[thumbnail]="thumbnail$ | async"
|
[thumbnail]="object?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/project-placeholder.svg'"
|
[defaultImage]="'assets/images/project-placeholder.svg'"
|
||||||
[alt]="'thumbnail.project.alt'"
|
[alt]="'thumbnail.project.alt'"
|
||||||
[placeholder]="'thumbnail.project.placeholder'">
|
[placeholder]="'thumbnail.project.placeholder'">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<!-- <div class="person-thumbnail pr-2">-->
|
<!-- <div class="person-thumbnail pr-2">-->
|
||||||
<!-- <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>-->
|
<!-- <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"
|
<ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
|
||||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.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';
|
||||||
@@ -113,11 +110,4 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
|||||||
modalComp.value = value;
|
modalComp.value = value;
|
||||||
return modalRef.result;
|
return modalRef.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
|
||||||
getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.dso).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
|
||||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.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';
|
||||||
@@ -108,11 +105,4 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
|
|||||||
modalComp.value = value;
|
modalComp.value = value;
|
||||||
return modalRef.result;
|
return modalRef.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
|
||||||
getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.dso).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { first, map } from 'rxjs/operators';
|
||||||
@@ -29,6 +29,12 @@ export class DeleteComColPageComponent<TDomain extends Community | Collection> i
|
|||||||
*/
|
*/
|
||||||
public dsoRD$: Observable<RemoteData<TDomain>>;
|
public dsoRD$: Observable<RemoteData<TDomain>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a delete operation is pending
|
||||||
|
* @type {BehaviorSubject<boolean>}
|
||||||
|
*/
|
||||||
|
public processing$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected dsoDataService: ComColDataService<TDomain>,
|
protected dsoDataService: ComColDataService<TDomain>,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@@ -48,6 +54,7 @@ export class DeleteComColPageComponent<TDomain extends Community | Collection> i
|
|||||||
* Deletes an existing DSO and redirects to the home page afterwards, showing a notification that states whether or not the deletion was successful
|
* Deletes an existing DSO and redirects to the home page afterwards, showing a notification that states whether or not the deletion was successful
|
||||||
*/
|
*/
|
||||||
onConfirm(dso: TDomain) {
|
onConfirm(dso: TDomain) {
|
||||||
|
this.processing$.next(true);
|
||||||
this.dsoDataService.delete(dso.id)
|
this.dsoDataService.delete(dso.id)
|
||||||
.pipe(getFirstCompletedRemoteData())
|
.pipe(getFirstCompletedRemoteData())
|
||||||
.subscribe((response: RemoteData<NoContent>) => {
|
.subscribe((response: RemoteData<NoContent>) => {
|
||||||
|
@@ -14,12 +14,12 @@
|
|||||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
[infiniteScrollContainer]="'.scrollable-menu'"
|
||||||
[fromRoot]="true"
|
[fromRoot]="true"
|
||||||
(scrolled)="onScrollDown()">
|
(scrolled)="onScrollDown()">
|
||||||
<ng-container *ngIf="listEntries">
|
<ng-container *ngIf="listEntries$ | async">
|
||||||
<button class="list-group-item list-group-item-action border-0 disabled"
|
<button class="list-group-item list-group-item-action border-0 disabled"
|
||||||
*ngIf="listEntries.length == 0">
|
*ngIf="(listEntries$ | async).length == 0">
|
||||||
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
||||||
</button>
|
</button>
|
||||||
<button *ngFor="let listEntry of listEntries"
|
<button *ngFor="let listEntry of (listEntries$ | async)"
|
||||||
class="list-group-item list-group-item-action border-0 list-entry"
|
class="list-group-item list-group-item-action border-0 list-entry"
|
||||||
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
|
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
|
||||||
title="{{ listEntry.indexableObject.name }}"
|
title="{{ listEntry.indexableObject.name }}"
|
||||||
|
@@ -92,12 +92,18 @@ describe('DSOSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('populating listEntries', () => {
|
describe('populating listEntries', () => {
|
||||||
it('should not be empty', () => {
|
it('should not be empty', (done) => {
|
||||||
expect(component.listEntries.length).toBeGreaterThan(0);
|
component.listEntries$.subscribe((listEntries) => {
|
||||||
|
expect(listEntries.length).toBeGreaterThan(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a combination of the current DSO and first page results', () => {
|
it('should contain a combination of the current DSO and first page results', (done) => {
|
||||||
expect(component.listEntries).toEqual([searchResult, ...firstPageResults]);
|
component.listEntries$.subscribe((listEntries) => {
|
||||||
|
expect(listEntries).toEqual([searchResult, ...firstPageResults]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when current page increases', () => {
|
describe('when current page increases', () => {
|
||||||
@@ -105,8 +111,11 @@ describe('DSOSelectorComponent', () => {
|
|||||||
component.currentPage$.next(2);
|
component.currentPage$.next(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a combination of the current DSO, as well as first and second page results', () => {
|
it('should contain a combination of the current DSO, as well as first and second page results', (done) => {
|
||||||
expect(component.listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]);
|
component.listEntries$.subscribe((listEntries) => {
|
||||||
|
expect(listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -81,7 +81,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List with search results of DSpace objects for the current query
|
* List with search results of DSpace objects for the current query
|
||||||
*/
|
*/
|
||||||
listEntries: SearchResult<DSpaceObject>[] = null;
|
listEntries$: BehaviorSubject<SearchResult<DSpaceObject>[]> = new BehaviorSubject(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current page to load
|
* The current page to load
|
||||||
@@ -160,7 +160,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
// The first page is loading, this means we should reset the list instead of adding to it
|
// The first page is loading, this means we should reset the list instead of adding to it
|
||||||
this.listEntries = null;
|
this.listEntries$.next(null);
|
||||||
}
|
}
|
||||||
return this.search(query, page).pipe(
|
return this.search(query, page).pipe(
|
||||||
map((rd) => {
|
map((rd) => {
|
||||||
@@ -181,15 +181,16 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe((rd) => {
|
).subscribe((rd) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
if (hasNoValue(this.listEntries)) {
|
const currentEntries = this.listEntries$.getValue();
|
||||||
this.listEntries = rd.payload.page;
|
if (hasNoValue(currentEntries)) {
|
||||||
|
this.listEntries$.next(rd.payload.page);
|
||||||
} else {
|
} else {
|
||||||
this.listEntries.push(...rd.payload.page);
|
this.listEntries$.next([...currentEntries, ...rd.payload.page]);
|
||||||
}
|
}
|
||||||
// Check if there are more pages available after the current one
|
// Check if there are more pages available after the current one
|
||||||
this.hasNextPage = rd.payload.totalElements > this.listEntries.length;
|
this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
|
||||||
} else {
|
} else {
|
||||||
this.listEntries = null;
|
this.listEntries$.next(null);
|
||||||
this.hasNextPage = false;
|
this.hasNextPage = false;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@@ -48,7 +48,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
</ds-dynamic-form>
|
</ds-dynamic-form>
|
||||||
|
<ng-content select="[additional]"></ng-content>
|
||||||
<ng-content *ngIf="!displaySubmit && !displayCancel"></ng-content>
|
<ng-content *ngIf="!displaySubmit && !displayCancel"></ng-content>
|
||||||
|
|
||||||
<div *ngIf="displaySubmit || displayCancel">
|
<div *ngIf="displaySubmit || displayCancel">
|
||||||
|
@@ -3,13 +3,27 @@
|
|||||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
(dsClickOutside)="close();">
|
(dsClickOutside)="close();">
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label *ngIf="label; else searchInput">
|
||||||
|
<span class="font-weight-bold">
|
||||||
|
{{label}}
|
||||||
|
</span>
|
||||||
|
<ng-container *ngTemplateOutlet="searchInput"></ng-container>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ng-template #searchInput>
|
||||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||||
class="form-control suggestion_input"
|
class="form-control suggestion_input"
|
||||||
[ngClass]="{'is-invalid': !valid}"
|
[ngClass]="{'is-invalid': !valid}"
|
||||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
[ngModelOptions]="{standalone: true}" autocomplete="off"
|
||||||
<input type="submit" class="d-none"/>
|
/>
|
||||||
|
</ng-template>
|
||||||
|
<label class="d-none">
|
||||||
|
<input type="submit"/>
|
||||||
|
<span>{{'search.filters.search.submit' | translate}}</span>
|
||||||
|
</label>
|
||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
<div class="dropdown-list">
|
<div class="dropdown-list">
|
||||||
<div *ngFor="let suggestionOption of suggestions">
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
|
@@ -53,6 +53,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
*/
|
*/
|
||||||
@Input() valid = true;
|
@Input() valid = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for the input field. Used for screen readers.
|
||||||
|
*/
|
||||||
|
@Input() label? = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output for when the form is submitted
|
* Output for when the form is submitted
|
||||||
*/
|
*/
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
|
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
@@ -117,17 +117,33 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('When a reloadedObject is emitted', () => {
|
describe('When a reloadedObject is emitted', () => {
|
||||||
|
let listableComponent;
|
||||||
|
let reloadedObject: any;
|
||||||
|
|
||||||
it('should re-instantiate the listable component ', fakeAsync(() => {
|
beforeEach(() => {
|
||||||
|
spyOn((comp as any), 'connectInputsAndOutputs').and.returnValue(null);
|
||||||
|
spyOn((comp as any).contentChange, 'emit').and.returnValue(null);
|
||||||
|
|
||||||
spyOn((comp as any), 'instantiateComponent').and.returnValue(null);
|
listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
|
||||||
|
reloadedObject = 'object';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass it on connectInputsAndOutputs', fakeAsync(() => {
|
||||||
|
expect((comp as any).connectInputsAndOutputs).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
|
|
||||||
const reloadedObject: any = 'object';
|
|
||||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
|
expect((comp as any).connectInputsAndOutputs).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should re-emit it as a contentChange', fakeAsync(() => {
|
||||||
|
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -3,10 +3,14 @@ import {
|
|||||||
ComponentFactoryResolver,
|
ComponentFactoryResolver,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy, OnInit,
|
OnDestroy,
|
||||||
Output, ViewChild
|
OnInit,
|
||||||
,
|
Output,
|
||||||
EventEmitter
|
ViewChild,
|
||||||
|
EventEmitter,
|
||||||
|
SimpleChanges,
|
||||||
|
OnChanges,
|
||||||
|
ComponentRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
@@ -15,7 +19,7 @@ import { getListableObjectComponent } from './listable-object.decorator';
|
|||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { ListableObjectDirective } from './listable-object.directive';
|
import { ListableObjectDirective } from './listable-object.directive';
|
||||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||||
import { hasValue } from '../../../empty.util';
|
import { hasValue, isNotEmpty } from '../../../empty.util';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
@@ -29,7 +33,7 @@ import { ThemeService } from '../../../theme-support/theme.service';
|
|||||||
/**
|
/**
|
||||||
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
|
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
|
||||||
*/
|
*/
|
||||||
export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy {
|
export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* The item or metadata to determine the component for
|
* The item or metadata to determine the component for
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +111,25 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
protected subs: Subscription[] = [];
|
protected subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reference to the dynamic component
|
||||||
|
*/
|
||||||
|
protected compRef: ComponentRef<Component>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of input and output names for the dynamic component
|
||||||
|
*/
|
||||||
|
protected inAndOutputNames: string[] = [
|
||||||
|
'object',
|
||||||
|
'index',
|
||||||
|
'linkType',
|
||||||
|
'listID',
|
||||||
|
'showLabel',
|
||||||
|
'context',
|
||||||
|
'viewMode',
|
||||||
|
'value',
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private themeService: ThemeService
|
private themeService: ThemeService
|
||||||
@@ -120,6 +143,15 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
this.instantiateComponent(this.object);
|
this.instantiateComponent(this.object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever the inputs change, update the inputs of the dynamic component
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
|
||||||
|
this.connectInputsAndOutputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.subs
|
this.subs
|
||||||
.filter((subscription) => hasValue(subscription))
|
.filter((subscription) => hasValue(subscription))
|
||||||
@@ -137,28 +169,22 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
viewContainerRef.clear();
|
viewContainerRef.clear();
|
||||||
|
|
||||||
const componentRef = viewContainerRef.createComponent(
|
this.compRef = viewContainerRef.createComponent(
|
||||||
componentFactory,
|
componentFactory,
|
||||||
0,
|
0,
|
||||||
undefined,
|
undefined,
|
||||||
[
|
[
|
||||||
[this.badges.nativeElement],
|
[this.badges.nativeElement],
|
||||||
]);
|
]);
|
||||||
(componentRef.instance as any).object = object;
|
|
||||||
(componentRef.instance as any).index = this.index;
|
|
||||||
(componentRef.instance as any).linkType = this.linkType;
|
|
||||||
(componentRef.instance as any).listID = this.listID;
|
|
||||||
(componentRef.instance as any).showLabel = this.showLabel;
|
|
||||||
(componentRef.instance as any).context = this.context;
|
|
||||||
(componentRef.instance as any).viewMode = this.viewMode;
|
|
||||||
(componentRef.instance as any).value = this.value;
|
|
||||||
|
|
||||||
if ((componentRef.instance as any).reloadedObject) {
|
this.connectInputsAndOutputs();
|
||||||
(componentRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
|
||||||
|
if ((this.compRef.instance as any).reloadedObject) {
|
||||||
|
(this.compRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
||||||
if (reloadedObject) {
|
if (reloadedObject) {
|
||||||
componentRef.destroy();
|
this.compRef.destroy();
|
||||||
this.object = reloadedObject;
|
this.object = reloadedObject;
|
||||||
this.instantiateComponent(reloadedObject);
|
this.connectInputsAndOutputs();
|
||||||
this.contentChange.emit(reloadedObject);
|
this.contentChange.emit(reloadedObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -187,4 +213,17 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
context: Context): GenericConstructor<Component> {
|
context: Context): GenericConstructor<Component> {
|
||||||
return getListableObjectComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
|
return getListableObjectComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect the in and outputs of this component to the dynamic component,
|
||||||
|
* to ensure they're in sync
|
||||||
|
*/
|
||||||
|
protected connectInputsAndOutputs(): void {
|
||||||
|
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
|
||||||
|
this.inAndOutputNames.forEach((name: any) => {
|
||||||
|
this.compRef.instance[name] = this[name];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -49,8 +49,8 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, true, true,
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item', null, true, true, true, followLink('bundles')),
|
followLink('item', {}, followLink('bundles')),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<div class="row mb-1">
|
<div class="row mb-1">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="item?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ng-container *ngVar="(getFiles() | async) as bitstreams">
|
<ng-container *ngVar="(getFiles() | async) as bitstreams">
|
||||||
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
|
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
|
||||||
|
@@ -126,13 +126,6 @@ describe('ItemDetailPreviewComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should get item thumbnail', (done) => {
|
|
||||||
component.getThumbnail().subscribe((thumbnail) => {
|
|
||||||
expect(thumbnail).toBeDefined();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get item bitstreams', (done) => {
|
it('should get item bitstreams', (done) => {
|
||||||
component.getFiles().subscribe((bitstreams) => {
|
component.getFiles().subscribe((bitstreams) => {
|
||||||
expect(bitstreams).toBeDefined();
|
expect(bitstreams).toBeDefined();
|
||||||
|
@@ -5,10 +5,7 @@ import { first } from 'rxjs/operators';
|
|||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||||
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import {
|
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
getFirstSucceededRemoteListPayload
|
|
||||||
} from '../../../../core/shared/operators';
|
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { fadeInOut } from '../../../animations/fade';
|
import { fadeInOut } from '../../../animations/fade';
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||||
@@ -57,11 +54,6 @@ export class ItemDetailPreviewComponent {
|
|||||||
*/
|
*/
|
||||||
public separator = ', ';
|
public separator = ', ';
|
||||||
|
|
||||||
/**
|
|
||||||
* The item's thumbnail
|
|
||||||
*/
|
|
||||||
public thumbnail$: Observable<Bitstream>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
@@ -86,13 +78,6 @@ export class ItemDetailPreviewComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
|
||||||
public getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.item).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
||||||
public getFiles(): Observable<Bitstream[]> {
|
public getFiles(): Observable<Bitstream[]> {
|
||||||
return this.bitstreamDataService
|
return this.bitstreamDataService
|
||||||
|
@@ -48,8 +48,8 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, true, true,
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item', null, true, true, true, followLink('bundles')),
|
followLink('item', {}, followLink('bundles')),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
|
@@ -6,13 +6,13 @@
|
|||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width">
|
class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -43,4 +43,3 @@
|
|||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -3,13 +3,11 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { SearchResult } from '../../search/search-result.model';
|
import { SearchResult } from '../../search/search-result.model';
|
||||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||||
import { hasValue } from '../../empty.util';
|
import { hasValue } from '../../empty.util';
|
||||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
import { TruncatableService } from '../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../truncatable/truncatable.service';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-result-grid-element',
|
selector: 'ds-search-result-grid-element',
|
||||||
@@ -66,11 +64,4 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
|
|||||||
private isCollapsed(): Observable<boolean> {
|
private isCollapsed(): Observable<boolean> {
|
||||||
return this.truncatableService.isCollapsed(this.dso.id);
|
return this.truncatableService.isCollapsed(this.dso.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
|
||||||
getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.dso as any).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -55,10 +55,7 @@ export class ClaimedApprovedSearchResultListElementComponent extends SearchResul
|
|||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso,
|
this.linkService.resolveLinks(this.dso,
|
||||||
followLink('workflowitem',
|
followLink('workflowitem',
|
||||||
null,
|
{ useCachedVersionIfAvailable: false },
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
followLink('item'),
|
followLink('item'),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
),
|
),
|
||||||
|
@@ -56,10 +56,7 @@ export class ClaimedDeclinedSearchResultListElementComponent extends SearchResul
|
|||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso,
|
this.linkService.resolveLinks(this.dso,
|
||||||
followLink('workflowitem',
|
followLink('workflowitem',
|
||||||
null,
|
{ useCachedVersionIfAvailable: false },
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
followLink('item'),
|
followLink('item'),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
),
|
),
|
||||||
|
@@ -50,7 +50,7 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, true, true,
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item'), followLink('submitter')
|
followLink('item'), followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
|
@@ -60,7 +60,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, true, true,
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item'), followLink('submitter')
|
followLink('item'), followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
|
@@ -8,15 +8,18 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
(click)="showMore()">{{"search.filters.filter.show-more"
|
(click)="showMore()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-more" | translate}}
|
||||||
|
</a>
|
||||||
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
(click)="showFirstPageOnly()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-less" | translate}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder' | translate"
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder' | translate"
|
||||||
|
[label]="'search.filters.filter.' + filterConfig.name + '.label' | translate"
|
||||||
[action]="currentUrl"
|
[action]="currentUrl"
|
||||||
[name]="filterConfig.paramName"
|
[name]="filterConfig.paramName"
|
||||||
[(ngModel)]="filter"
|
[(ngModel)]="filter"
|
||||||
|
@@ -8,11 +8,13 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
(click)="showMore()">{{"search.filters.filter.show-more"
|
(click)="showMore()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-more" | translate}}
|
||||||
|
</a>
|
||||||
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
(click)="showFirstPageOnly()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-less" | translate}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
||||||
|
[tabIndex]="-1"
|
||||||
[routerLink]="[searchLink]"
|
[routerLink]="[searchLink]"
|
||||||
[queryParams]="addQueryParams" queryParamsHandling="merge">
|
[queryParams]="addQueryParams" queryParamsHandling="merge">
|
||||||
|
<label class="mb-0">
|
||||||
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
|
||||||
<span class="filter-value px-1">
|
<span class="filter-value px-1">
|
||||||
{{ 'search.filters.' + filterConfig.name + '.' + filterValue.value | translate: {default: filterValue.value} }}
|
{{ 'search.filters.' + filterConfig.name + '.' + filterValue.value | translate: {default: filterValue.value} }}
|
||||||
</span>
|
</span>
|
||||||
|
</label>
|
||||||
<span class="float-right filter-value-count ml-auto">
|
<span class="float-right filter-value-count ml-auto">
|
||||||
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
<a class="d-flex flex-row"
|
<a class="d-flex flex-row"
|
||||||
|
[tabIndex]="-1"
|
||||||
[routerLink]="[searchLink]"
|
[routerLink]="[searchLink]"
|
||||||
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
||||||
|
<label class="mb-0">
|
||||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||||
<span class="filter-value pl-1 text-capitalize">
|
<span class="filter-value pl-1 text-capitalize">
|
||||||
{{ 'search.filters.' + filterConfig.name + '.' + selectedValue.value | translate: {default: selectedValue.label} }}
|
{{ 'search.filters.' + filterConfig.name + '.' + selectedValue.value | translate: {default: selectedValue.label} }}
|
||||||
</span>
|
</span>
|
||||||
|
</label>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,17 +1,21 @@
|
|||||||
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async">
|
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async"
|
||||||
<div (click)="toggle()" class="filter-name">
|
[id]="regionId" [attr.aria-labelledby]="toggleId" [ngClass]="{ 'focus': focusBox }" role="region">
|
||||||
|
<button (click)="toggle()" (focusin)="focusBox = true" (focusout)="focusBox = false"
|
||||||
|
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
|
||||||
|
[attr.aria-expanded]="false"
|
||||||
|
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
|
||||||
|
>
|
||||||
<h5 class="d-inline-block mb-0">
|
<h5 class="d-inline-block mb-0">
|
||||||
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
||||||
</h5>
|
</h5>
|
||||||
<span class="filter-toggle fas float-right"
|
<span class="filter-toggle flex-grow-1 fas p-auto"
|
||||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"
|
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"
|
||||||
[title]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
|
[title]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
|
||||||
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
|
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
|
||||||
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
|
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
|
||||||
class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }">
|
||||||
<ds-search-facet-filter-wrapper
|
<ds-search-facet-filter-wrapper
|
||||||
[filterConfig]="filter"
|
[filterConfig]="filter"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
|
@@ -1,10 +1,36 @@
|
|||||||
:host .facet-filter {
|
:host .facet-filter {
|
||||||
border: 1px solid var(--bs-light);
|
border: 1px solid var(--bs-light);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.search-filter-wrapper.closed {
|
line-height: 0;
|
||||||
|
|
||||||
|
.search-filter-wrapper {
|
||||||
|
line-height: var(--bs-line-height-base);
|
||||||
|
&.closed {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
&.notab {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-toggle {
|
.filter-toggle {
|
||||||
line-height: var(--bs-line-height-base);
|
line-height: var(--bs-line-height-base);
|
||||||
|
text-align: right;
|
||||||
|
position: relative;
|
||||||
|
top: -0.125rem; // Fix weird outline shape in Chrome
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--bs-input-btn-focus-box-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import { SearchFilterConfig } from '../../search-filter-config.model';
|
|||||||
import { FilterType } from '../../filter-type.model';
|
import { FilterType } from '../../filter-type.model';
|
||||||
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SequenceService } from '../../../../core/shared/sequence.service';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
@@ -50,12 +51,15 @@ describe('SearchFilterComponent', () => {
|
|||||||
|
|
||||||
};
|
};
|
||||||
let filterService;
|
let filterService;
|
||||||
|
let sequenceService;
|
||||||
const mockResults = observableOf(['test', 'data']);
|
const mockResults = observableOf(['test', 'data']);
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
getFacetValuesFor: (filter) => mockResults
|
getFacetValuesFor: (filter) => mockResults
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
sequenceService = jasmine.createSpyObj('sequenceService', { next: 17 });
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
declarations: [SearchFilterComponent],
|
declarations: [SearchFilterComponent],
|
||||||
@@ -65,7 +69,8 @@ describe('SearchFilterComponent', () => {
|
|||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: mockFilterService
|
useValue: mockFilterService
|
||||||
},
|
},
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
|
{ provide: SequenceService, useValue: sequenceService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFilterComponent, {
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
@@ -81,6 +86,12 @@ describe('SearchFilterComponent', () => {
|
|||||||
filterService = (comp as any).filterService;
|
filterService = (comp as any).filterService;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate unique IDs', () => {
|
||||||
|
expect(sequenceService.next).toHaveBeenCalled();
|
||||||
|
expect(comp.toggleId).toContain('17');
|
||||||
|
expect(comp.regionId).toContain('17');
|
||||||
|
});
|
||||||
|
|
||||||
describe('when the toggle method is triggered', () => {
|
describe('when the toggle method is triggered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(filterService, 'toggle');
|
spyOn(filterService, 'toggle');
|
||||||
|
@@ -10,6 +10,7 @@ import { isNotEmpty } from '../../../empty.util';
|
|||||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SequenceService } from '../../../../core/shared/sequence.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filter',
|
selector: 'ds-search-filter',
|
||||||
@@ -37,6 +38,16 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the filter controls should be hidden & removed from the tablist
|
||||||
|
*/
|
||||||
|
notab: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the filter toggle button is focused
|
||||||
|
*/
|
||||||
|
focusBox = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the filter is currently collapsed in the store
|
* Emits true when the filter is currently collapsed in the store
|
||||||
*/
|
*/
|
||||||
@@ -52,10 +63,15 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
active$: Observable<boolean>;
|
active$: Observable<boolean>;
|
||||||
|
|
||||||
|
private readonly sequenceId: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private filterService: SearchFilterService,
|
private filterService: SearchFilterService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService,
|
||||||
|
private sequenceService: SequenceService,
|
||||||
|
) {
|
||||||
|
this.sequenceId = this.sequenceService.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +128,9 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
if (event.fromState === 'collapsed') {
|
if (event.fromState === 'collapsed') {
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
}
|
}
|
||||||
|
if (event.toState === 'collapsed') {
|
||||||
|
this.notab = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +141,17 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
if (event.toState === 'collapsed') {
|
if (event.toState === 'collapsed') {
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
if (event.fromState === 'collapsed') {
|
||||||
|
this.notab = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get regionId(): string {
|
||||||
|
return `search-filter-region-${this.sequenceId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get toggleId(): string {
|
||||||
|
return `search-filter-toggle-${this.sequenceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -8,15 +8,18 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
(click)="showMore()">{{"search.filters.filter.show-more"
|
(click)="showMore()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-more" | translate}}
|
||||||
|
</a>
|
||||||
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
(click)="showFirstPageOnly()" href="javascript:void(0);">
|
||||||
| translate}}</a>
|
{{"search.filters.filter.show-less" | translate}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder' | translate"
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder' | translate"
|
||||||
|
[label]="'search.filters.filter.' + filterConfig.name + '.label' | translate"
|
||||||
[action]="currentUrl"
|
[action]="currentUrl"
|
||||||
[name]="filterConfig.paramName"
|
[name]="filterConfig.paramName"
|
||||||
[(ngModel)]="filter"
|
[(ngModel)]="filter"
|
||||||
|
@@ -2,25 +2,42 @@
|
|||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<form #form="ngForm" (ngSubmit)="onSubmit()" class="add-filter row"
|
<form #form="ngForm" (ngSubmit)="onSubmit()" class="add-filter row"
|
||||||
[action]="currentUrl">
|
[action]="currentUrl">
|
||||||
<div class="col-6">
|
<div class="col-6 form-group mb-0">
|
||||||
|
<label>
|
||||||
|
<span class="font-weight-bold">
|
||||||
|
{{'search.filters.filter.' + filterConfig.name + '.min.label' | translate}}
|
||||||
|
</span>
|
||||||
<input type="text" [(ngModel)]="range[0]" [name]="filterConfig.paramName + '.min'"
|
<input type="text" [(ngModel)]="range[0]" [name]="filterConfig.paramName + '.min'"
|
||||||
class="form-control" (blur)="onSubmit()"
|
class="form-control" (blur)="onSubmit()"
|
||||||
aria-label="Mininum value"
|
aria-label="Mininum value"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.min.placeholder'| translate"/>
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.min.placeholder' | translate"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
|
<label>
|
||||||
|
<span class="font-weight-bold">
|
||||||
|
{{'search.filters.filter.' + filterConfig.name + '.max.label' | translate}}
|
||||||
|
</span>
|
||||||
<input type="text" [(ngModel)]="range[1]" [name]="filterConfig.paramName + '.max'"
|
<input type="text" [(ngModel)]="range[1]" [name]="filterConfig.paramName + '.max'"
|
||||||
class="form-control" (blur)="onSubmit()"
|
class="form-control" (blur)="onSubmit()"
|
||||||
aria-label="Maximum value"
|
aria-label="Maximum value"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.max.placeholder'| translate"/>
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.max.placeholder' | translate"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="d-none">
|
||||||
<input type="submit" class="d-none"/>
|
<input type="submit" class="d-none"/>
|
||||||
|
<span>{{'search.filters.search.submit' | translate}}</span>
|
||||||
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ng-container *ngIf="shouldShowSlider()">
|
<ng-container *ngIf="shouldShowSlider()">
|
||||||
<nouislider [connect]="true" [min]="min" [max]="max" [step]="1"
|
<nouislider [connect]="true" [min]="min" [max]="max" [step]="1"
|
||||||
[(ngModel)]="range" (change)="onSubmit()" ngDefaultControl></nouislider>
|
[dsDebounce]="250" (onDebounce)="onSubmit()"
|
||||||
|
(keydown)="startKeyboardControl()" (keyup)="stopKeyboardControl()"
|
||||||
|
[(ngModel)]="range" ngDefaultControl>
|
||||||
|
</nouislider>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-shadow: var(--bs-input-btn-focus-box-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user