forked from hazza/dspace-angular
Merge pull request #117 from artlowel/dso-list-component
Live REST API & Dso list component
This commit is contained in:
@@ -2,10 +2,10 @@ module.exports = {
|
|||||||
// The REST API server settings.
|
// The REST API server settings.
|
||||||
"rest": {
|
"rest": {
|
||||||
"ssl": false,
|
"ssl": false,
|
||||||
"address": "localhost",
|
"address": "dspace7.4science.it",
|
||||||
"port": 3000,
|
"port": 80,
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
"nameSpace": "/api"
|
"nameSpace": "/dspace-spring-rest/api"
|
||||||
},
|
},
|
||||||
// Angular2 UI server settings.
|
// Angular2 UI server settings.
|
||||||
"ui": {
|
"ui": {
|
||||||
|
@@ -7,7 +7,12 @@
|
|||||||
"collection": {
|
"collection": {
|
||||||
"page": {
|
"page": {
|
||||||
"news": "News",
|
"news": "News",
|
||||||
"license": "License"
|
"license": "License",
|
||||||
|
"browse": {
|
||||||
|
"recent": {
|
||||||
|
"head": "Recent Submissions"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community": {
|
"community": {
|
||||||
@@ -45,6 +50,7 @@
|
|||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"results-per-page": "Results Per Page",
|
"results-per-page": "Results Per Page",
|
||||||
|
"sort-direction": "Sort Options",
|
||||||
"showing": {
|
"showing": {
|
||||||
"label": "Now showing items ",
|
"label": "Now showing items ",
|
||||||
"detail": "{{ range }} of {{ total }}"
|
"detail": "{{ range }} of {{ total }}"
|
||||||
|
@@ -1,32 +1,44 @@
|
|||||||
<div class="collection-page" *ngIf="collectionData.hasSucceeded | async">
|
<div class="collection-page">
|
||||||
<!-- Collection Name -->
|
<div *ngIf="collectionData.hasSucceeded | async">
|
||||||
<ds-comcol-page-header
|
<!-- Collection Name -->
|
||||||
[name]="(collectionData.payload | async)?.name">
|
<ds-comcol-page-header
|
||||||
</ds-comcol-page-header>
|
[name]="(collectionData.payload | async)?.name">
|
||||||
<!-- Collection logo -->
|
</ds-comcol-page-header>
|
||||||
<ds-comcol-page-logo *ngIf="logoData"
|
<!-- Collection logo -->
|
||||||
[logo]="logoData.payload | async"
|
<ds-comcol-page-logo *ngIf="logoData"
|
||||||
[alternateText]="'Collection Logo'">
|
[logo]="logoData.payload | async"
|
||||||
</ds-comcol-page-logo>
|
[alternateText]="'Collection Logo'">
|
||||||
<!-- Introductionary text -->
|
</ds-comcol-page-logo>
|
||||||
<ds-comcol-page-content
|
<!-- Introductionary text -->
|
||||||
|
<ds-comcol-page-content
|
||||||
[content]="(collectionData.payload | async)?.introductoryText"
|
[content]="(collectionData.payload | async)?.introductoryText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="(collectionData.payload | async)?.sidebarText"
|
[content]="(collectionData.payload | async)?.sidebarText"
|
||||||
[hasInnerHtml]="true"
|
[hasInnerHtml]="true"
|
||||||
[title]="'community.page.news'">
|
[title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="(collectionData.payload | async)?.copyrightText"
|
[content]="(collectionData.payload | async)?.copyrightText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Licence -->
|
<!-- Licence -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="(collectionData.payload | async)?.license"
|
[content]="(collectionData.payload | async)?.license"
|
||||||
[title]="'collection.page.license'">
|
[title]="'collection.page.license'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div *ngIf="itemData.hasSucceeded | async">
|
||||||
|
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||||
|
<ds-object-list [config]="config" [sortConfig]="sortConfig"
|
||||||
|
[objects]="itemData" [hideGear]="true"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortDirectionChange($event)"></ds-object-list>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { Collection } from "../core/shared/collection.model";
|
import { Collection } from "../core/shared/collection.model";
|
||||||
@@ -6,36 +9,93 @@ import { Bitstream } from "../core/shared/bitstream.model";
|
|||||||
import { RemoteData } from "../core/data/remote-data";
|
import { RemoteData } from "../core/data/remote-data";
|
||||||
import { CollectionDataService } from "../core/data/collection-data.service";
|
import { CollectionDataService } from "../core/data/collection-data.service";
|
||||||
import { Subscription } from "rxjs/Subscription";
|
import { Subscription } from "rxjs/Subscription";
|
||||||
|
import { ItemDataService } from "../core/data/item-data.service";
|
||||||
|
import { Item } from "../core/shared/item.model";
|
||||||
|
import { SortOptions, SortDirection } from "../core/cache/models/sort-options.model";
|
||||||
|
import { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
|
||||||
|
import { Observable } from "rxjs/Observable";
|
||||||
|
import { hasValue } from "../shared/empty.util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
styleUrls: ['./collection-page.component.css'],
|
styleUrls: ['./collection-page.component.css'],
|
||||||
templateUrl: './collection-page.component.html',
|
templateUrl: './collection-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||||
collectionData: RemoteData<Collection>;
|
collectionData: RemoteData<Collection>;
|
||||||
|
itemData: RemoteData<Item[]>;
|
||||||
logoData: RemoteData<Bitstream>;
|
logoData: RemoteData<Bitstream>;
|
||||||
|
config : PaginationComponentOptions;
|
||||||
|
sortConfig : SortOptions;
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
private collectionId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private ref: ChangeDetectorRef,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
this.universalInit();
|
this.universalInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.params.subscribe((params: Params) => {
|
this.subs.push(this.route.params.map((params: Params) => params['id'] )
|
||||||
this.collectionData = this.collectionDataService.findById(params['id']);
|
.subscribe((id: string) => {
|
||||||
this.subs.push(this.collectionData.payload
|
this.collectionId = id;
|
||||||
.subscribe(collection => this.logoData = collection.logo));
|
this.collectionData = this.collectionDataService.findById(this.collectionId);
|
||||||
});
|
this.subs.push(this.collectionData.payload
|
||||||
|
.subscribe(collection => this.logoData = collection.logo));
|
||||||
|
|
||||||
|
this.config = new PaginationComponentOptions();
|
||||||
|
this.config.id = "collection-browse";
|
||||||
|
this.config.pageSizeOptions = [ 4 ];
|
||||||
|
this.config.pageSize = 4;
|
||||||
|
this.sortConfig = new SortOptions();
|
||||||
|
|
||||||
|
this.updateResults();
|
||||||
|
}));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.forEach(sub => sub.unsubscribe());
|
this.subs
|
||||||
|
.filter(sub => hasValue(sub))
|
||||||
|
.forEach(sub => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
universalInit() {
|
universalInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPageChange(currentPage: number): void {
|
||||||
|
this.config.currentPage = currentPage;
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange(elementsPerPage: number): void {
|
||||||
|
this.config.pageSize = elementsPerPage;
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortDirectionChange(sortDirection: SortDirection): void {
|
||||||
|
this.sortConfig = new SortOptions(this.sortConfig.field, sortDirection);
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortFieldChange(field: string): void {
|
||||||
|
this.sortConfig = new SortOptions(field, this.sortConfig.direction);
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResults() {
|
||||||
|
this.itemData = undefined;
|
||||||
|
this.itemData = this.itemDataService.findAll({
|
||||||
|
scopeID: this.collectionId,
|
||||||
|
currentPage: this.config.currentPage,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
sort: this.sortConfig
|
||||||
|
});
|
||||||
|
// this.ref.detectChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from "@ngx-translate/core";
|
||||||
|
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
import { CollectionPageComponent } from './collection-page.component';
|
import { CollectionPageComponent } from './collection-page.component';
|
||||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
|||||||
imports: [
|
imports: [
|
||||||
CollectionPageRoutingModule,
|
CollectionPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
TranslateModule,
|
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
TranslateModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
|
@@ -6,6 +6,7 @@ import { Bitstream } from "../core/shared/bitstream.model";
|
|||||||
import { RemoteData } from "../core/data/remote-data";
|
import { RemoteData } from "../core/data/remote-data";
|
||||||
import { CommunityDataService } from "../core/data/community-data.service";
|
import { CommunityDataService } from "../core/data/community-data.service";
|
||||||
import { Subscription } from "rxjs/Subscription";
|
import { Subscription } from "rxjs/Subscription";
|
||||||
|
import { hasValue } from "../shared/empty.util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page',
|
selector: 'ds-community-page',
|
||||||
@@ -33,9 +34,11 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.forEach(sub => sub.unsubscribe());
|
this.subs
|
||||||
|
.filter(sub => hasValue(sub))
|
||||||
|
.forEach(sub => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
universalInit() {
|
universalInit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ export const getMapsTo = function(target: any) {
|
|||||||
return Reflect.getOwnMetadata(mapsToMetadataKey, target);
|
return Reflect.getOwnMetadata(mapsToMetadataKey, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relationship = function(value: ResourceType): any {
|
export const relationship = function(value: ResourceType, isList: boolean = false): any {
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
if (!target || !propertyKey) {
|
if (!target || !propertyKey) {
|
||||||
return;
|
return;
|
||||||
@@ -28,11 +28,11 @@ export const relationship = function(value: ResourceType): any {
|
|||||||
}
|
}
|
||||||
relationshipMap.set(target.constructor, metaDataList);
|
relationshipMap.set(target.constructor, metaDataList);
|
||||||
|
|
||||||
return Reflect.metadata(relationshipKey, value).apply(this, arguments);
|
return Reflect.metadata(relationshipKey, { resourceType: value, isList }).apply(this, arguments);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getResourceType = function(target: any, propertyKey: string) {
|
export const getRelationMetadata = function(target: any, propertyKey: string) {
|
||||||
return Reflect.getMetadata(relationshipKey, target, propertyKey);
|
return Reflect.getMetadata(relationshipKey, target, propertyKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ import { ErrorResponse, SuccessResponse } from "../response-cache.models";
|
|||||||
import { Observable } from "rxjs/Observable";
|
import { Observable } from "rxjs/Observable";
|
||||||
import { RemoteData } from "../../data/remote-data";
|
import { RemoteData } from "../../data/remote-data";
|
||||||
import { GenericConstructor } from "../../shared/generic-constructor";
|
import { GenericConstructor } from "../../shared/generic-constructor";
|
||||||
import { getMapsTo, getResourceType, getRelationships } from "./build-decorators";
|
import { getMapsTo, getRelationMetadata, getRelationships } from "./build-decorators";
|
||||||
import { NormalizedObjectFactory } from "../models/normalized-object-factory";
|
import { NormalizedObjectFactory } from "../models/normalized-object-factory";
|
||||||
import { Request } from "../../data/request.models";
|
import { Request } from "../../data/request.models";
|
||||||
|
|
||||||
@@ -55,17 +55,54 @@ export class RemoteDataBuildService {
|
|||||||
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
|
|
||||||
const payload = this.objectCache.getBySelfLink<TNormalized>(href, normalizedType)
|
const statusCode = responseCacheObs
|
||||||
.map((normalized: TNormalized) => {
|
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
.distinctUntilChanged();
|
||||||
|
|
||||||
|
const pageInfo = responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
||||||
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
|
||||||
|
//always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
|
const payload =
|
||||||
|
Observable.combineLatest(
|
||||||
|
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType).startWith(undefined),
|
||||||
|
responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||||
|
.flatMap((resourceUUIDs: Array<string>) => {
|
||||||
|
if (isNotEmpty(resourceUUIDs)) {
|
||||||
|
return this.objectCache.get(resourceUUIDs[0], normalizedType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Observable.of(undefined);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.startWith(undefined),
|
||||||
|
(fromSelfLink, fromResponse) => {
|
||||||
|
if (hasValue(fromSelfLink)) {
|
||||||
|
return fromSelfLink;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return fromResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).filter(normalized => hasValue(normalized))
|
||||||
|
.map((normalized: TNormalized) => {
|
||||||
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
href,
|
href,
|
||||||
requestPending,
|
requestPending,
|
||||||
responsePending,
|
responsePending,
|
||||||
isSuccessFul,
|
isSuccessFul,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
statusCode,
|
||||||
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,6 +127,15 @@ export class RemoteDataBuildService {
|
|||||||
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
|
|
||||||
|
const statusCode = responseCacheObs
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
|
||||||
|
const pageInfo = responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
||||||
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
|
||||||
const payload = responseCacheObs
|
const payload = responseCacheObs
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||||
@@ -109,6 +155,8 @@ export class RemoteDataBuildService {
|
|||||||
responsePending,
|
responsePending,
|
||||||
isSuccessFul,
|
isSuccessFul,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
statusCode,
|
||||||
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,7 +169,7 @@ export class RemoteDataBuildService {
|
|||||||
|
|
||||||
relationships.forEach((relationship: string) => {
|
relationships.forEach((relationship: string) => {
|
||||||
if (hasValue(normalized[relationship])) {
|
if (hasValue(normalized[relationship])) {
|
||||||
const resourceType = getResourceType(normalized, relationship);
|
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
||||||
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
||||||
if (Array.isArray(normalized[relationship])) {
|
if (Array.isArray(normalized[relationship])) {
|
||||||
// without the setTimeout, the actions inside requestService.configure
|
// without the setTimeout, the actions inside requestService.configure
|
||||||
@@ -137,7 +185,12 @@ export class RemoteDataBuildService {
|
|||||||
rdArr.push(this.buildSingle(href, resourceConstructor));
|
rdArr.push(this.buildSingle(href, resourceConstructor));
|
||||||
});
|
});
|
||||||
|
|
||||||
links[relationship] = this.aggregate(rdArr);
|
if (rdArr.length === 1) {
|
||||||
|
links[relationship] = rdArr[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
links[relationship] = this.aggregate(rdArr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// without the setTimeout, the actions inside requestService.configure
|
// without the setTimeout, the actions inside requestService.configure
|
||||||
@@ -146,7 +199,14 @@ export class RemoteDataBuildService {
|
|||||||
this.requestService.configure(new Request(normalized[relationship]));
|
this.requestService.configure(new Request(normalized[relationship]));
|
||||||
},0);
|
},0);
|
||||||
|
|
||||||
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
|
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
|
||||||
|
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
|
||||||
|
// but it should still be built as a list
|
||||||
|
if (isList) {
|
||||||
|
links[relationship] = this.buildList(normalized[relationship], resourceConstructor);
|
||||||
|
} else {
|
||||||
|
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -183,6 +243,20 @@ export class RemoteDataBuildService {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const statusCode = Observable.combineLatest(
|
||||||
|
...input.map(rd => rd.statusCode),
|
||||||
|
).map((...statusCodes) => statusCodes
|
||||||
|
.map((code, idx) => {
|
||||||
|
if (hasValue(code)) {
|
||||||
|
return `[${idx}]: ${code}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(c => hasValue(c))
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageInfo = Observable.of(undefined);
|
||||||
|
|
||||||
const payload = <Observable<T[]>> Observable.combineLatest(
|
const payload = <Observable<T[]>> Observable.combineLatest(
|
||||||
...input.map(rd => rd.payload)
|
...input.map(rd => rd.payload)
|
||||||
);
|
);
|
||||||
@@ -196,6 +270,8 @@ export class RemoteDataBuildService {
|
|||||||
responsePending,
|
responsePending,
|
||||||
isSuccessFul,
|
isSuccessFul,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
statusCode,
|
||||||
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
11
src/app/core/cache/models/normalized-bitstream-format.model.ts
vendored
Normal file
11
src/app/core/cache/models/normalized-bitstream-format.model.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { NormalizedObject } from "./normalized-object.model";
|
||||||
|
import { inheritSerialization } from "cerialize";
|
||||||
|
|
||||||
|
@inheritSerialization(NormalizedObject)
|
||||||
|
export class NormalizedBitstreamFormat extends NormalizedObject {
|
||||||
|
//TODO this class was created as a placeholder when we connected to the live rest api
|
||||||
|
|
||||||
|
get uuid(): string {
|
||||||
|
return this.self;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,50 +1,60 @@
|
|||||||
import { inheritSerialization, autoserialize } from "cerialize";
|
import { inheritSerialization, autoserialize } from "cerialize";
|
||||||
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
||||||
import { Bitstream } from "../../shared/bitstream.model";
|
import { Bitstream } from "../../shared/bitstream.model";
|
||||||
import { mapsTo } from "../builders/build-decorators";
|
import { mapsTo, relationship } from "../builders/build-decorators";
|
||||||
|
import { ResourceType } from "../../shared/resource-type";
|
||||||
|
|
||||||
@mapsTo(Bitstream)
|
@mapsTo(Bitstream)
|
||||||
@inheritSerialization(NormalizedDSpaceObject)
|
@inheritSerialization(NormalizedDSpaceObject)
|
||||||
export class NormalizedBitstream extends NormalizedDSpaceObject {
|
export class NormalizedBitstream extends NormalizedDSpaceObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of this bitstream in bytes(?)
|
* The size of this bitstream in bytes
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
size: number;
|
sizeBytes: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relative path to this Bitstream's file
|
* The relative path to this Bitstream's file
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
url: string;
|
retrieve: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mime type of this Bitstream
|
* The mime type of this Bitstream
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
mimetype: string;
|
mimetype: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The format of this Bitstream
|
* The format of this Bitstream
|
||||||
*/
|
*/
|
||||||
format: string;
|
@autoserialize
|
||||||
|
format: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description of this Bitstream
|
* The description of this Bitstream
|
||||||
*/
|
*/
|
||||||
description: string;
|
@autoserialize
|
||||||
|
description: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Bundles that are direct parents of this Bitstream
|
* An array of Bundles that are direct parents of this Bitstream
|
||||||
*/
|
*/
|
||||||
parents: Array<string>;
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Item, true)
|
||||||
|
parents: Array<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Bundle that owns this Bitstream
|
* The Bundle that owns this Bitstream
|
||||||
*/
|
*/
|
||||||
owner: string;
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Item, false)
|
||||||
|
owner: string;
|
||||||
|
|
||||||
@autoserialize
|
/**
|
||||||
retrieve: string;
|
* The name of the Bundle this Bitstream is part of
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
bundleName: string;
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
|
|||||||
* The primary bitstream of this Bundle
|
* The primary bitstream of this Bundle
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Bitstream)
|
@relationship(ResourceType.Bitstream, false)
|
||||||
primaryBitstream: string;
|
primaryBitstream: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +25,6 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
|
|||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Bitstream)
|
@relationship(ResourceType.Bitstream, true)
|
||||||
bitstreams: Array<string>;
|
bitstreams: Array<string>;
|
||||||
}
|
}
|
||||||
|
@@ -18,21 +18,25 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
|
|||||||
* The Bitstream that represents the logo of this Collection
|
* The Bitstream that represents the logo of this Collection
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Bitstream)
|
@relationship(ResourceType.Bitstream, false)
|
||||||
logo: string;
|
logo: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Collections that are direct parents of this Collection
|
* An array of Communities that are direct parents of this Collection
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Community, true)
|
||||||
parents: Array<string>;
|
parents: Array<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Collection that owns this Collection
|
* The Community that owns this Collection
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Community, false)
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Item)
|
@relationship(ResourceType.Item, true)
|
||||||
items: Array<string>;
|
items: Array<string>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -18,21 +18,25 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
|
|||||||
* The Bitstream that represents the logo of this Community
|
* The Bitstream that represents the logo of this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Bitstream)
|
@relationship(ResourceType.Bitstream, false)
|
||||||
logo: string;
|
logo: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Communities that are direct parents of this Community
|
* An array of Communities that are direct parents of this Community
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Community, true)
|
||||||
parents: Array<string>;
|
parents: Array<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Community that owns this Community
|
* The Community that owns this Community
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Community, false)
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Collection)
|
@relationship(ResourceType.Collection, true)
|
||||||
collections: Array<string>;
|
collections: Array<string>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,36 @@
|
|||||||
import { autoserialize, autoserializeAs } from "cerialize";
|
import { autoserialize, autoserializeAs, inheritSerialization } from "cerialize";
|
||||||
import { CacheableObject } from "../object-cache.reducer";
|
|
||||||
import { Metadatum } from "../../shared/metadatum.model";
|
import { Metadatum } from "../../shared/metadatum.model";
|
||||||
import { ResourceType } from "../../shared/resource-type";
|
import { ResourceType } from "../../shared/resource-type";
|
||||||
|
import { NormalizedObject } from "./normalized-object.model";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a DSpaceObject.
|
* An abstract model class for a DSpaceObject.
|
||||||
*/
|
*/
|
||||||
export abstract class NormalizedDSpaceObject implements CacheableObject {
|
export abstract class NormalizedDSpaceObject extends NormalizedObject{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the rest endpoint where this object can be found
|
||||||
|
*
|
||||||
|
* Repeated here to make the serialization work,
|
||||||
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
self: string;
|
self: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The human-readable identifier of this DSpaceObject
|
* The human-readable identifier of this DSpaceObject
|
||||||
|
*
|
||||||
|
* Currently mapped to uuid but left in to leave room
|
||||||
|
* for a shorter, more user friendly type of id
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserializeAs(String, 'uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The universally unique identifier of this DSpaceObject
|
* The universally unique identifier of this DSpaceObject
|
||||||
|
*
|
||||||
|
* Repeated here to make the serialization work,
|
||||||
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { inheritSerialization, autoserialize } from "cerialize";
|
import { inheritSerialization, autoserialize, autoserializeAs } from "cerialize";
|
||||||
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
||||||
import { Item } from "../../shared/item.model";
|
import { Item } from "../../shared/item.model";
|
||||||
import { mapsTo, relationship } from "../builders/build-decorators";
|
import { mapsTo, relationship } from "../builders/build-decorators";
|
||||||
@@ -17,31 +17,41 @@ export class NormalizedItem extends NormalizedDSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Date of the last modification of this Item
|
* The Date of the last modification of this Item
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if this Item is currently archived or not
|
* A boolean representing if this Item is currently archived or not
|
||||||
*/
|
*/
|
||||||
|
@autoserializeAs(Boolean, 'inArchive')
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if this Item is currently discoverable or not
|
||||||
|
*/
|
||||||
|
@autoserializeAs(Boolean, 'discoverable')
|
||||||
|
isDiscoverable: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if this Item is currently withdrawn or not
|
* A boolean representing if this Item is currently withdrawn or not
|
||||||
*/
|
*/
|
||||||
|
@autoserializeAs(Boolean, 'withdrawn')
|
||||||
isWithdrawn: boolean;
|
isWithdrawn: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Collections that are direct parents of this Item
|
* An array of Collections that are direct parents of this Item
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Collection)
|
@relationship(ResourceType.Collection, true)
|
||||||
parents: Array<string>;
|
parents: Array<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Collection that owns this Item
|
* The Collection that owns this Item
|
||||||
*/
|
*/
|
||||||
owner: string;
|
@relationship(ResourceType.Collection, false)
|
||||||
|
owningCollection: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@relationship(ResourceType.Bundle)
|
@relationship(ResourceType.Bitstream, true)
|
||||||
bundles: Array<string>;
|
bitstreams: Array<string>;
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,20 @@ import { NormalizedCollection } from "./normalized-collection.model";
|
|||||||
import { GenericConstructor } from "../../shared/generic-constructor";
|
import { GenericConstructor } from "../../shared/generic-constructor";
|
||||||
import { NormalizedCommunity } from "./normalized-community.model";
|
import { NormalizedCommunity } from "./normalized-community.model";
|
||||||
import { ResourceType } from "../../shared/resource-type";
|
import { ResourceType } from "../../shared/resource-type";
|
||||||
|
import { NormalizedObject } from "./normalized-object.model";
|
||||||
|
import { NormalizedBitstreamFormat } from "./normalized-bitstream-format.model";
|
||||||
|
|
||||||
export class NormalizedObjectFactory {
|
export class NormalizedObjectFactory {
|
||||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedDSpaceObject> {
|
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ResourceType.Bitstream: {
|
case ResourceType.Bitstream: {
|
||||||
return NormalizedBitstream
|
return NormalizedBitstream
|
||||||
}
|
}
|
||||||
|
// commented out for now, bitstreamformats aren't used in the UI yet
|
||||||
|
// and slow things down noticeably
|
||||||
|
// case ResourceType.BitstreamFormat: {
|
||||||
|
// return NormalizedBitstreamFormat
|
||||||
|
// }
|
||||||
case ResourceType.Bundle: {
|
case ResourceType.Bundle: {
|
||||||
return NormalizedBundle
|
return NormalizedBundle
|
||||||
}
|
}
|
||||||
|
20
src/app/core/cache/models/normalized-object.model.ts
vendored
Normal file
20
src/app/core/cache/models/normalized-object.model.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { CacheableObject } from "../object-cache.reducer";
|
||||||
|
import { autoserialize } from "cerialize";
|
||||||
|
/**
|
||||||
|
* An abstract model class for a NormalizedObject.
|
||||||
|
*/
|
||||||
|
export abstract class NormalizedObject implements CacheableObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the rest endpoint where this object can be found
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The universally unique identifier of this Object
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
}
|
@@ -4,6 +4,8 @@ export enum SortDirection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SortOptions {
|
export class SortOptions {
|
||||||
field: string = "id";
|
|
||||||
direction: SortDirection = SortDirection.Ascending
|
constructor (public field: string = "name", public direction : SortDirection = SortDirection.Ascending) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ describe("ObjectCacheService", () => {
|
|||||||
let store: Store<ObjectCacheState>;
|
let store: Store<ObjectCacheState>;
|
||||||
|
|
||||||
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
const requestHref = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const objectToCache = {
|
const objectToCache = {
|
||||||
@@ -44,8 +45,8 @@ describe("ObjectCacheService", () => {
|
|||||||
describe("add", () => {
|
describe("add", () => {
|
||||||
it("should dispatch an ADD action with the object to add, the time to live, and the current timestamp", () => {
|
it("should dispatch an ADD action with the object to add, the time to live, and the current timestamp", () => {
|
||||||
|
|
||||||
service.add(objectToCache, msToLive);
|
service.add(objectToCache, msToLive, requestHref);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive));
|
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestHref));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
20
src/app/core/cache/response-cache.models.ts
vendored
20
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,18 +1,28 @@
|
|||||||
|
import { RequestError } from "../data/request.models";
|
||||||
|
import { PageInfo } from "../shared/page-info.model";
|
||||||
|
|
||||||
export class Response {
|
export class Response {
|
||||||
constructor(public isSuccessful: boolean) {}
|
constructor(
|
||||||
|
public isSuccessful: boolean,
|
||||||
|
public statusCode: string
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SuccessResponse extends Response {
|
export class SuccessResponse extends Response {
|
||||||
constructor(public resourceUUIDs: Array<String>) {
|
constructor(
|
||||||
super(true);
|
public resourceUUIDs: Array<String>,
|
||||||
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ErrorResponse extends Response {
|
export class ErrorResponse extends Response {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
|
||||||
constructor(error: Error) {
|
constructor(error: RequestError) {
|
||||||
super(false);
|
super(false, error.statusText);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.errorMessage = error.message;
|
this.errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import { ItemDataService } from "./data/item-data.service";
|
|||||||
import { RequestService } from "./data/request.service";
|
import { RequestService } from "./data/request.service";
|
||||||
import { RemoteDataBuildService } from "./cache/builders/remote-data-build.service";
|
import { RemoteDataBuildService } from "./cache/builders/remote-data-build.service";
|
||||||
import { CommunityDataService } from "./data/community-data.service";
|
import { CommunityDataService } from "./data/community-data.service";
|
||||||
import { PaginationOptions } from "./cache/models/pagination-options.model";
|
import { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -33,7 +33,7 @@ const PROVIDERS = [
|
|||||||
ItemDataService,
|
ItemDataService,
|
||||||
DSpaceRESTv2Service,
|
DSpaceRESTv2Service,
|
||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationOptions,
|
PaginationComponentOptions,
|
||||||
ResponseCacheService,
|
ResponseCacheService,
|
||||||
RequestService,
|
RequestService,
|
||||||
RemoteDataBuildService
|
RemoteDataBuildService
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
import { DataService } from "./data.service";
|
import { DataService } from "./data.service";
|
||||||
import { Collection } from "../shared/collection.model";
|
import { Collection } from "../shared/collection.model";
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
@@ -8,19 +8,22 @@ import { NormalizedCollection } from "../cache/models/normalized-collection.mode
|
|||||||
import { CoreState } from "../core.reducers";
|
import { CoreState } from "../core.reducers";
|
||||||
import { RequestService } from "./request.service";
|
import { RequestService } from "./request.service";
|
||||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
||||||
protected endpoint = '/collections';
|
protected resourceEndpoint = '/core/collections';
|
||||||
|
protected browseEndpoint = '/discover/browses/dateissued/collections';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>
|
protected store: Store<CoreState>,
|
||||||
|
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||||
) {
|
) {
|
||||||
super(NormalizedCollection);
|
super(NormalizedCollection, EnvConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
import { DataService } from "./data.service";
|
import { DataService } from "./data.service";
|
||||||
import { Community } from "../shared/community.model";
|
import { Community } from "../shared/community.model";
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
@@ -8,19 +8,22 @@ import { NormalizedCommunity } from "../cache/models/normalized-community.model"
|
|||||||
import { CoreState } from "../core.reducers";
|
import { CoreState } from "../core.reducers";
|
||||||
import { RequestService } from "./request.service";
|
import { RequestService } from "./request.service";
|
||||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
||||||
protected endpoint = '/communities';
|
protected resourceEndpoint = '/core/communities';
|
||||||
|
protected browseEndpoint = '/discover/browses/dateissued/communities';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>
|
protected store: Store<CoreState>,
|
||||||
|
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||||
) {
|
) {
|
||||||
super(NormalizedCommunity);
|
super(NormalizedCommunity, EnvConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,18 @@
|
|||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
import { ResponseCacheService } from "../cache/response-cache.service";
|
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||||
import { CacheableObject } from "../cache/object-cache.reducer";
|
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||||
import { hasValue } from "../../shared/empty.util";
|
import { hasValue, isNotEmpty } from "../../shared/empty.util";
|
||||||
import { RemoteData } from "./remote-data";
|
import { RemoteData } from "./remote-data";
|
||||||
import { FindAllRequest, FindByIDRequest, Request } from "./request.models";
|
import { FindAllOptions, FindAllRequest, FindByIDRequest, Request } from "./request.models";
|
||||||
import { Store } from "@ngrx/store";
|
import { Store } from "@ngrx/store";
|
||||||
import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
|
import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
|
||||||
import { CoreState } from "../core.reducers";
|
import { CoreState } from "../core.reducers";
|
||||||
import { RequestService } from "./request.service";
|
import { RequestService } from "./request.service";
|
||||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||||
import { GenericConstructor } from "../shared/generic-constructor";
|
import { GenericConstructor } from "../shared/generic-constructor";
|
||||||
|
import { Inject } from "@angular/core";
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||||
|
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
|
||||||
|
|
||||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
||||||
protected abstract objectCache: ObjectCacheService;
|
protected abstract objectCache: ObjectCacheService;
|
||||||
@@ -17,30 +20,61 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
protected abstract endpoint: string;
|
protected abstract resourceEndpoint: string;
|
||||||
|
protected abstract browseEndpoint: string;
|
||||||
|
|
||||||
constructor(private normalizedResourceType: GenericConstructor<TNormalized>) {
|
constructor(
|
||||||
|
private normalizedResourceType: GenericConstructor<TNormalized>,
|
||||||
|
protected EnvConfig: GlobalConfig
|
||||||
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindAllHref(scopeID?): string {
|
protected getFindAllHref(options: FindAllOptions = {}): string {
|
||||||
let result = this.endpoint;
|
let result;
|
||||||
if (hasValue(scopeID)) {
|
let args = [];
|
||||||
result += `?scope=${scopeID}`
|
|
||||||
|
if (hasValue(options.scopeID)) {
|
||||||
|
result = this.browseEndpoint;
|
||||||
|
args.push(`scope=${options.scopeID}`);
|
||||||
}
|
}
|
||||||
return result;
|
else {
|
||||||
|
result = this.resourceEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.currentPage) && typeof options.currentPage === "number") {
|
||||||
|
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
||||||
|
args.push(`page=${options.currentPage - 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.elementsPerPage)) {
|
||||||
|
args.push(`size=${options.elementsPerPage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.sort)) {
|
||||||
|
let direction = 'asc';
|
||||||
|
if (options.sort.direction === 1) {
|
||||||
|
direction = 'desc';
|
||||||
|
}
|
||||||
|
args.push(`sort=${options.sort.field},${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
result = `${result}?${args.join('&')}`;
|
||||||
|
}
|
||||||
|
return new RESTURLCombiner(this.EnvConfig, result).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(scopeID?: string): RemoteData<Array<TDomain>> {
|
findAll(options: FindAllOptions = {}): RemoteData<Array<TDomain>> {
|
||||||
const href = this.getFindAllHref(scopeID);
|
const href = this.getFindAllHref(options);
|
||||||
const request = new FindAllRequest(href, scopeID);
|
const request = new FindAllRequest(href, options);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType);
|
return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType);
|
||||||
// return this.rdbService.buildList(href);
|
// return this.rdbService.buildList(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindByIDHref(resourceID): string {
|
protected getFindByIDHref(resourceID): string {
|
||||||
return `${this.endpoint}/${resourceID}`;
|
return new RESTURLCombiner(this.EnvConfig, `${this.resourceEndpoint}/${resourceID}`).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): RemoteData<TDomain> {
|
findById(id: string): RemoteData<TDomain> {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
import { DataService } from "./data.service";
|
import { DataService } from "./data.service";
|
||||||
import { Item } from "../shared/item.model";
|
import { Item } from "../shared/item.model";
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
@@ -8,18 +8,21 @@ import { CoreState } from "../core.reducers";
|
|||||||
import { NormalizedItem } from "../cache/models/normalized-item.model";
|
import { NormalizedItem } from "../cache/models/normalized-item.model";
|
||||||
import { RequestService } from "./request.service";
|
import { RequestService } from "./request.service";
|
||||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
protected endpoint = '/items';
|
protected resourceEndpoint = '/core/items';
|
||||||
|
protected browseEndpoint = '/discover/browses/dateissued/items';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>
|
protected store: Store<CoreState>,
|
||||||
|
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||||
) {
|
) {
|
||||||
super(NormalizedItem);
|
super(NormalizedItem, EnvConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
import { PageInfo } from "../shared/page-info.model";
|
||||||
|
|
||||||
export enum RemoteDataState {
|
export enum RemoteDataState {
|
||||||
RequestPending,
|
RequestPending = <any> "RequestPending",
|
||||||
ResponsePending,
|
ResponsePending = <any> "ResponsePending",
|
||||||
Failed,
|
Failed = <any> "Failed",
|
||||||
Success
|
Success = <any> "Success"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +18,8 @@ export class RemoteData<T> {
|
|||||||
private responsePending: Observable<boolean>,
|
private responsePending: Observable<boolean>,
|
||||||
private isSuccessFul: Observable<boolean>,
|
private isSuccessFul: Observable<boolean>,
|
||||||
public errorMessage: Observable<string>,
|
public errorMessage: Observable<string>,
|
||||||
|
public statusCode: Observable<string>,
|
||||||
|
public pageInfo: Observable<PageInfo>,
|
||||||
public payload: Observable<T>
|
public payload: Observable<T>
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Injectable, Inject } from "@angular/core";
|
import { Injectable, Inject } from "@angular/core";
|
||||||
import { Actions, Effect } from "@ngrx/effects";
|
import { Actions, Effect } from "@ngrx/effects";
|
||||||
import { Store } from "@ngrx/store";
|
|
||||||
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
|
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
|
||||||
@@ -10,7 +9,7 @@ import { Observable } from "rxjs";
|
|||||||
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
|
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from "../../shared/empty.util";
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from "../../shared/empty.util";
|
||||||
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
|
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
|
||||||
import { RequestState, RequestEntry } from "./request.reducer";
|
import { RequestEntry } from "./request.reducer";
|
||||||
import {
|
import {
|
||||||
RequestActionTypes, RequestExecuteAction,
|
RequestActionTypes, RequestExecuteAction,
|
||||||
RequestCompleteAction
|
RequestCompleteAction
|
||||||
@@ -19,11 +18,31 @@ import { ResponseCacheService } from "../cache/response-cache.service";
|
|||||||
import { RequestService } from "./request.service";
|
import { RequestService } from "./request.service";
|
||||||
import { NormalizedObjectFactory } from "../cache/models/normalized-object-factory";
|
import { NormalizedObjectFactory } from "../cache/models/normalized-object-factory";
|
||||||
import { ResourceType } from "../shared/resource-type";
|
import { ResourceType } from "../shared/resource-type";
|
||||||
|
import { RequestError } from "./request.models";
|
||||||
|
import { PageInfo } from "../shared/page-info.model";
|
||||||
|
import { NormalizedObject } from "../cache/models/normalized-object.model";
|
||||||
|
|
||||||
function isObjectLevel(halObj: any) {
|
function isObjectLevel(halObj: any) {
|
||||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPaginatedResponse(halObj: any) {
|
||||||
|
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenSingleKeyObject(obj: any): any {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
if (keys.length !== 1) {
|
||||||
|
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
|
||||||
|
}
|
||||||
|
return obj[keys[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProcessRequestDTO {
|
||||||
|
[key: string]: NormalizedObject[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestEffects {
|
export class RequestEffects {
|
||||||
|
|
||||||
@@ -33,8 +52,7 @@ export class RequestEffects {
|
|||||||
private restApi: DSpaceRESTv2Service,
|
private restApi: DSpaceRESTv2Service,
|
||||||
private objectCache: ObjectCacheService,
|
private objectCache: ObjectCacheService,
|
||||||
private responseCache: ResponseCacheService,
|
private responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService
|
||||||
private store: Store<RequestState>
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Effect() execute = this.actions$
|
@Effect() execute = this.actions$
|
||||||
@@ -45,83 +63,102 @@ export class RequestEffects {
|
|||||||
})
|
})
|
||||||
.flatMap((entry: RequestEntry) => {
|
.flatMap((entry: RequestEntry) => {
|
||||||
return this.restApi.get(entry.request.href)
|
return this.restApi.get(entry.request.href)
|
||||||
.map((data: DSpaceRESTV2Response) => this.processEmbedded(data._embedded, entry.request.href))
|
.map((data: DSpaceRESTV2Response) => {
|
||||||
.map((ids: Array<string>) => new SuccessResponse(ids))
|
const processRequestDTO = this.process(data.payload, entry.request.href);
|
||||||
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
const uuids = flattenSingleKeyObject(processRequestDTO).map(no => no.uuid);
|
||||||
|
return new SuccessResponse(uuids, data.statusCode, this.processPageInfo(data.payload.page))
|
||||||
|
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||||
.map((response: Response) => new RequestCompleteAction(entry.request.href))
|
.map((response: Response) => new RequestCompleteAction(entry.request.href))
|
||||||
.catch((error: Error) => Observable.of(new ErrorResponse(error))
|
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
|
||||||
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||||
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
|
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
|
||||||
});
|
});
|
||||||
|
|
||||||
protected processEmbedded(_embedded: any, requestHref): Array<string> {
|
protected process(data: any, requestHref: string): ProcessRequestDTO {
|
||||||
|
|
||||||
if (isNotEmpty(_embedded)) {
|
if (isNotEmpty(data)) {
|
||||||
if (isObjectLevel(_embedded)) {
|
if (isPaginatedResponse(data)) {
|
||||||
return this.deserializeAndCache(_embedded, requestHref);
|
return this.process(data._embedded, requestHref);
|
||||||
|
}
|
||||||
|
else if (isObjectLevel(data)) {
|
||||||
|
return { "topLevel": this.deserializeAndCache(data, requestHref) };
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let uuids = [];
|
let result = new ProcessRequestDTO();
|
||||||
Object.keys(_embedded)
|
if (Array.isArray(data)) {
|
||||||
.filter(property => _embedded.hasOwnProperty(property))
|
result['topLevel'] = [];
|
||||||
.forEach(property => {
|
data.forEach(datum => {
|
||||||
uuids = [...uuids, ...this.deserializeAndCache(_embedded[property], requestHref)];
|
if (isPaginatedResponse(datum)) {
|
||||||
|
const obj = this.process(datum, requestHref);
|
||||||
|
result['topLevel'] = [...result['topLevel'], ...flattenSingleKeyObject(obj)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result['topLevel'] = [...result['topLevel'], ...this.deserializeAndCache(datum, requestHref)];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return uuids;
|
}
|
||||||
|
else {
|
||||||
|
Object.keys(data)
|
||||||
|
.filter(property => data.hasOwnProperty(property))
|
||||||
|
.filter(property => hasValue(data[property]))
|
||||||
|
.forEach(property => {
|
||||||
|
if (isPaginatedResponse(data[property])) {
|
||||||
|
const obj = this.process(data[property], requestHref);
|
||||||
|
result[property] = flattenSingleKeyObject(obj);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result[property] = this.deserializeAndCache(data[property], requestHref);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deserializeAndCache(obj, requestHref): Array<string> {
|
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
|
||||||
let type: ResourceType;
|
if(Array.isArray(obj)) {
|
||||||
const isArray = Array.isArray(obj);
|
let result = [];
|
||||||
|
obj.forEach(o => result = [...result, ...this.deserializeAndCache(o, requestHref)])
|
||||||
if (isArray && isEmpty(obj)) {
|
return result;
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray) {
|
|
||||||
type = obj[0]["type"];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
type = obj["type"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let type: ResourceType = obj["type"];
|
||||||
if (hasValue(type)) {
|
if (hasValue(type)) {
|
||||||
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
|
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
|
||||||
|
|
||||||
if (hasValue(normObjConstructor)) {
|
if (hasValue(normObjConstructor)) {
|
||||||
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
|
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
|
||||||
|
|
||||||
if (isArray) {
|
let processed;
|
||||||
obj.forEach(o => {
|
if (isNotEmpty(obj._embedded)) {
|
||||||
if (isNotEmpty(o._embedded)) {
|
processed = this.process(obj._embedded, requestHref);
|
||||||
this.processEmbedded(o._embedded, requestHref);
|
}
|
||||||
}
|
let normalizedObj = serializer.deserialize(obj);
|
||||||
|
|
||||||
|
if (isNotEmpty(processed)) {
|
||||||
|
let linksOnly = {};
|
||||||
|
Object.keys(processed).forEach(key => {
|
||||||
|
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
|
||||||
});
|
});
|
||||||
const normalizedObjArr = serializer.deserializeArray(obj);
|
Object.assign(normalizedObj, linksOnly);
|
||||||
normalizedObjArr.forEach(t => this.addToObjectCache(t, requestHref));
|
|
||||||
return normalizedObjArr.map(t => t.uuid);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (isNotEmpty(obj._embedded)) {
|
|
||||||
this.processEmbedded(obj._embedded, requestHref);
|
|
||||||
}
|
|
||||||
const normalizedObj = serializer.deserialize(obj);
|
|
||||||
this.addToObjectCache(normalizedObj, requestHref);
|
|
||||||
return [normalizedObj.uuid];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addToObjectCache(normalizedObj, requestHref);
|
||||||
|
return [normalizedObj];
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//TODO move check to Validator?
|
//TODO move check to Validator?
|
||||||
throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//TODO move check to Validator
|
//TODO move check to Validator
|
||||||
throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,4 +168,14 @@ export class RequestEffects {
|
|||||||
}
|
}
|
||||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected processPageInfo(pageObj: any): PageInfo {
|
||||||
|
if (isNotEmpty(pageObj)) {
|
||||||
|
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { SortOptions } from "../cache/models/sort-options.model";
|
import { SortOptions } from "../cache/models/sort-options.model";
|
||||||
import { PaginationOptions } from "../cache/models/pagination-options.model";
|
import { PaginationComponentOptions } from "../../shared/pagination/pagination-component-options.model";
|
||||||
import { GenericConstructor } from "../shared/generic-constructor";
|
import { GenericConstructor } from "../shared/generic-constructor";
|
||||||
|
|
||||||
export class Request<T> {
|
export class Request<T> {
|
||||||
@@ -17,13 +17,22 @@ export class FindByIDRequest<T> extends Request<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FindAllOptions {
|
||||||
|
scopeID?: string;
|
||||||
|
elementsPerPage?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
sort?: SortOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export class FindAllRequest<T> extends Request<T> {
|
export class FindAllRequest<T> extends Request<T> {
|
||||||
constructor(
|
constructor(
|
||||||
href: string,
|
href: string,
|
||||||
public scopeID?: string,
|
public options?: FindAllOptions,
|
||||||
public paginationOptions?: PaginationOptions,
|
|
||||||
public sortOptions?: SortOptions
|
|
||||||
) {
|
) {
|
||||||
super(href);
|
super(href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RequestError extends Error {
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
export interface DSpaceRESTV2Response {
|
export interface DSpaceRESTV2Response {
|
||||||
_embedded?: any;
|
payload: {
|
||||||
_links?: any;
|
_embedded?: any;
|
||||||
|
_links?: any;
|
||||||
|
page?: any;
|
||||||
|
},
|
||||||
|
statusCode: string
|
||||||
}
|
}
|
||||||
|
@@ -60,8 +60,8 @@ describe("DSpaceRESTv2Serializer", () => {
|
|||||||
it("should turn a model in to a valid document", () => {
|
it("should turn a model in to a valid document", () => {
|
||||||
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
||||||
const doc = serializer.serialize(testModels[0]);
|
const doc = serializer.serialize(testModels[0]);
|
||||||
expect(testModels[0].id).toBe(doc._embedded.id);
|
expect(testModels[0].id).toBe(doc.id);
|
||||||
expect(testModels[0].name).toBe(doc._embedded.name);
|
expect(testModels[0].name).toBe(doc.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -72,10 +72,10 @@ describe("DSpaceRESTv2Serializer", () => {
|
|||||||
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
||||||
const doc = serializer.serializeArray(testModels);
|
const doc = serializer.serializeArray(testModels);
|
||||||
|
|
||||||
expect(testModels[0].id).toBe(doc._embedded[0].id);
|
expect(testModels[0].id).toBe(doc[0].id);
|
||||||
expect(testModels[0].name).toBe(doc._embedded[0].name);
|
expect(testModels[0].name).toBe(doc[0].name);
|
||||||
expect(testModels[1].id).toBe(doc._embedded[1].id);
|
expect(testModels[1].id).toBe(doc[1].id);
|
||||||
expect(testModels[1].name).toBe(doc._embedded[1].name);
|
expect(testModels[1].name).toBe(doc[1].name);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -26,10 +26,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
|||||||
* @param model The model to serialize
|
* @param model The model to serialize
|
||||||
* @returns An object to send to the backend
|
* @returns An object to send to the backend
|
||||||
*/
|
*/
|
||||||
serialize(model: T): DSpaceRESTV2Response {
|
serialize(model: T): any {
|
||||||
return {
|
return Serialize(model, this.modelType);
|
||||||
"_embedded": Serialize(model, this.modelType)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,10 +36,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
|||||||
* @param models The array of models to serialize
|
* @param models The array of models to serialize
|
||||||
* @returns An object to send to the backend
|
* @returns An object to send to the backend
|
||||||
*/
|
*/
|
||||||
serializeArray(models: Array<T>): DSpaceRESTV2Response {
|
serializeArray(models: Array<T>): any {
|
||||||
return {
|
return Serialize(models, this.modelType);
|
||||||
"_embedded": Serialize(models, this.modelType)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
|
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to access DSpace's REST API
|
* Service to access DSpace's REST API
|
||||||
@@ -17,16 +18,16 @@ export class DSpaceRESTv2Service {
|
|||||||
/**
|
/**
|
||||||
* Performs a request to the REST API with the `get` http method.
|
* Performs a request to the REST API with the `get` http method.
|
||||||
*
|
*
|
||||||
* @param relativeURL
|
* @param absoluteURL
|
||||||
* A URL, relative to the basepath of the rest api
|
* A URL
|
||||||
* @param options
|
* @param options
|
||||||
* A RequestOptionsArgs object, with options for the http call.
|
* A RequestOptionsArgs object, with options for the http call.
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observablse<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
get(relativeURL: string, options?: RequestOptionsArgs): Observable<string> {
|
get(absoluteURL: string, options?: RequestOptionsArgs): Observable<DSpaceRESTV2Response> {
|
||||||
return this.http.get(new RESTURLCombiner(this.EnvConfig, relativeURL).toString(), options)
|
return this.http.get(absoluteURL, options)
|
||||||
.map(res => res.json())
|
.map(res => ({ payload: res.json(), statusCode: res.statusText }))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log('Error: ', err);
|
console.log('Error: ', err);
|
||||||
return Observable.throw(err);
|
return Observable.throw(err);
|
||||||
|
@@ -1,42 +1,42 @@
|
|||||||
import { DSpaceObject } from "./dspace-object.model";
|
import { DSpaceObject } from "./dspace-object.model";
|
||||||
import { Bundle } from "./bundle.model";
|
|
||||||
import { RemoteData } from "../data/remote-data";
|
import { RemoteData } from "../data/remote-data";
|
||||||
|
import { Item } from "./item.model";
|
||||||
|
|
||||||
export class Bitstream extends DSpaceObject {
|
export class Bitstream extends DSpaceObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of this bitstream in bytes(?)
|
* The size of this bitstream in bytes
|
||||||
*/
|
*/
|
||||||
size: number;
|
sizeBytes: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relative path to this Bitstream's file
|
* The mime type of this Bitstream
|
||||||
*/
|
*/
|
||||||
url: string;
|
mimetype: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mime type of this Bitstream
|
* The description of this Bitstream
|
||||||
*/
|
*/
|
||||||
mimetype: string;
|
description: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description of this Bitstream
|
* The name of the Bundle this Bitstream is part of
|
||||||
*/
|
*/
|
||||||
description: string;
|
bundleName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Bundles that are direct parents of this Bitstream
|
* An array of Items that are direct parents of this Bitstream
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<Bundle[]>;
|
parents: RemoteData<Item[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Bundle that owns this Bitstream
|
* The Bundle that owns this Bitstream
|
||||||
*/
|
*/
|
||||||
owner: Bundle;
|
owner: RemoteData<Item>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Bundle that owns this Bitstream
|
* The URL to retrieve this Bitstream's file
|
||||||
*/
|
*/
|
||||||
retrieve: string;
|
retrieve: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ export class Bundle extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Item that owns this Bundle
|
* The Item that owns this Bundle
|
||||||
*/
|
*/
|
||||||
owner: Item;
|
owner: RemoteData<Item>;
|
||||||
|
|
||||||
bitstreams: RemoteData<Bitstream[]>
|
bitstreams: RemoteData<Bitstream[]>
|
||||||
|
|
||||||
|
@@ -63,7 +63,7 @@ export class Collection extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Collection that owns this Collection
|
* The Collection that owns this Collection
|
||||||
*/
|
*/
|
||||||
owner: Collection;
|
owner: RemoteData<Collection>;
|
||||||
|
|
||||||
items: RemoteData<Item[]>;
|
items: RemoteData<Item[]>;
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@ export class Community extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Community that owns this Community
|
* The Community that owns this Community
|
||||||
*/
|
*/
|
||||||
owner: Community;
|
owner: RemoteData<Community>;
|
||||||
|
|
||||||
collections: RemoteData<Collection[]>;
|
collections: RemoteData<Collection[]>;
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ export abstract class DSpaceObject implements CacheableObject {
|
|||||||
/**
|
/**
|
||||||
* The DSpaceObject that owns this DSpaceObject
|
* The DSpaceObject that owns this DSpaceObject
|
||||||
*/
|
*/
|
||||||
owner: DSpaceObject;
|
owner: RemoteData<DSpaceObject>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a metadata field by key and language
|
* Find a metadata field by key and language
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { TestBed, async } from '@angular/core/testing';
|
|
||||||
import { Item } from "./item.model";
|
import { Item } from "./item.model";
|
||||||
import { Bundle } from "./bundle.model";
|
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { RemoteData } from "../data/remote-data";
|
import { RemoteData } from "../data/remote-data";
|
||||||
import { Bitstream } from "./bitstream.model";
|
import { Bitstream } from "./bitstream.model";
|
||||||
|
import { isEmpty } from "../../shared/empty.util";
|
||||||
|
import { PageInfo } from "./page-info.model";
|
||||||
|
|
||||||
|
|
||||||
describe('Item', () => {
|
describe('Item', () => {
|
||||||
@@ -17,23 +17,25 @@ describe('Item', () => {
|
|||||||
const bitstream2Path = "otherfile.doc";
|
const bitstream2Path = "otherfile.doc";
|
||||||
|
|
||||||
const nonExistingBundleName = "c1e568f7-d14e-496b-bdd7-07026998cc00";
|
const nonExistingBundleName = "c1e568f7-d14e-496b-bdd7-07026998cc00";
|
||||||
let remoteBundles;
|
let bitstreams;
|
||||||
let thumbnailBundle;
|
let remoteDataThumbnail;
|
||||||
let originalBundle;
|
let remoteDataFiles;
|
||||||
|
let remoteDataAll;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const thumbnail = {
|
const thumbnail = {
|
||||||
retrieve: thumbnailPath
|
retrieve: thumbnailPath
|
||||||
};
|
};
|
||||||
|
|
||||||
const bitstreams = [{
|
bitstreams = [{
|
||||||
retrieve: bitstream1Path
|
retrieve: bitstream1Path
|
||||||
}, {
|
}, {
|
||||||
retrieve: bitstream2Path
|
retrieve: bitstream2Path
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const remoteDataThumbnail = createRemoteDataObject(thumbnail);
|
remoteDataThumbnail = createRemoteDataObject(thumbnail);
|
||||||
const remoteDataFiles = createRemoteDataObject(bitstreams);
|
remoteDataFiles = createRemoteDataObject(bitstreams);
|
||||||
|
remoteDataAll = createRemoteDataObject([...bitstreams, thumbnail]);
|
||||||
|
|
||||||
|
|
||||||
// Create Bundles
|
// Create Bundles
|
||||||
@@ -50,32 +52,30 @@ describe('Item', () => {
|
|||||||
bitstreams: remoteDataFiles
|
bitstreams: remoteDataFiles
|
||||||
}];
|
}];
|
||||||
|
|
||||||
remoteBundles = createRemoteDataObject(bundles);
|
|
||||||
|
|
||||||
item = Object.assign(new Item(), { bundles: remoteBundles });
|
item = Object.assign(new Item(), { bitstreams: remoteDataAll});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return the bundle with the given name of this item when the bundle exists', () => {
|
it('should return the bitstreams related to this item with the specified bundle name', () => {
|
||||||
let name: string = thumbnailBundleName;
|
const bitObs: Observable<Bitstream[]> = item.getBitstreamsByBundleName(thumbnailBundleName);
|
||||||
let bundle: Observable<Bundle> = item.getBundle(name);
|
bitObs.take(1).subscribe(bs =>
|
||||||
bundle.map(b => expect(b.name).toBe(name));
|
expect(bs.every(b => b.name === thumbnailBundleName)).toBeTruthy());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when no bundle with this name exists for this item', () => {
|
it('should return an empty array when no bitstreams with this bundleName exist for this item', () => {
|
||||||
let name: string = nonExistingBundleName;
|
const bitstreams: Observable<Bitstream[]> = item.getBitstreamsByBundleName(nonExistingBundleName);
|
||||||
let bundle: Observable<Bundle> = item.getBundle(name);
|
bitstreams.take(1).subscribe(bs => expect(isEmpty(bs)).toBeTruthy());
|
||||||
bundle.map(b => expect(b).toBeUndefined());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("get thumbnail", () => {
|
describe("get thumbnail", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(item, 'getBundle').and.returnValue(Observable.of(thumbnailBundle));
|
spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of([remoteDataThumbnail]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the thumbnail (the primaryBitstream in the bundle "THUMBNAIL") of this item', () => {
|
it('should return the thumbnail of this item', () => {
|
||||||
let path: string = thumbnailPath;
|
let path: string = thumbnailPath;
|
||||||
let bitstream: Observable<Bitstream> = item.getThumbnail();
|
let bitstream: Observable<Bitstream> = item.getThumbnail();
|
||||||
bitstream.map(b => expect(b.retrieve).toBe(path));
|
bitstream.map(b => expect(b.retrieve).toBe(path));
|
||||||
@@ -85,10 +85,10 @@ describe('Item', () => {
|
|||||||
|
|
||||||
describe("get files", () => {
|
describe("get files", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(item, 'getBundle').and.returnValue(Observable.of(originalBundle));
|
spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of(bitstreams));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all files in the ORIGINAL bundle', () => {
|
it('should return all bitstreams with "ORIGINAL" as bundleName', () => {
|
||||||
let paths = [bitstream1Path, bitstream2Path];
|
let paths = [bitstream1Path, bitstream2Path];
|
||||||
|
|
||||||
let files: Observable<Bitstream[]> = item.getFiles();
|
let files: Observable<Bitstream[]> = item.getFiles();
|
||||||
@@ -110,6 +110,23 @@ describe('Item', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function createRemoteDataObject(object: Object) {
|
function createRemoteDataObject(object: Object) {
|
||||||
return new RemoteData("", Observable.of(false), Observable.of(false), Observable.of(true), Observable.of(undefined), Observable.of(object));
|
const self = "";
|
||||||
|
const requestPending = Observable.of(false);
|
||||||
|
const responsePending = Observable.of(false);
|
||||||
|
const isSuccessful = Observable.of(true);
|
||||||
|
const errorMessage = Observable.of(undefined);
|
||||||
|
const statusCode = Observable.of("200");
|
||||||
|
const pageInfo = Observable.of(new PageInfo());
|
||||||
|
const payload = Observable.of(object);
|
||||||
|
return new RemoteData(
|
||||||
|
self,
|
||||||
|
requestPending,
|
||||||
|
responsePending,
|
||||||
|
isSuccessful,
|
||||||
|
errorMessage,
|
||||||
|
statusCode,
|
||||||
|
pageInfo,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import { DSpaceObject } from "./dspace-object.model";
|
import { DSpaceObject } from "./dspace-object.model";
|
||||||
import { Collection } from "./collection.model";
|
import { Collection } from "./collection.model";
|
||||||
import { RemoteData } from "../data/remote-data";
|
import { RemoteData } from "../data/remote-data";
|
||||||
import { Bundle } from "./bundle.model";
|
|
||||||
import { Bitstream } from "./bitstream.model";
|
import { Bitstream } from "./bitstream.model";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { hasValue } from "../../shared/empty.util";
|
import { isNotEmpty } from "../../shared/empty.util";
|
||||||
|
|
||||||
export class Item extends DSpaceObject {
|
export class Item extends DSpaceObject {
|
||||||
|
|
||||||
@@ -23,6 +22,11 @@ export class Item extends DSpaceObject {
|
|||||||
*/
|
*/
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if this Item is currently discoverable or not
|
||||||
|
*/
|
||||||
|
isDiscoverable: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if this Item is currently withdrawn or not
|
* A boolean representing if this Item is currently withdrawn or not
|
||||||
*/
|
*/
|
||||||
@@ -36,9 +40,13 @@ export class Item extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Collection that owns this Item
|
* The Collection that owns this Item
|
||||||
*/
|
*/
|
||||||
owner: Collection;
|
owningCollection: RemoteData<Collection>;
|
||||||
|
|
||||||
bundles: RemoteData<Bundle[]>;
|
get owner(): RemoteData<Collection> {
|
||||||
|
return this.owningCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitstreams: RemoteData<Bitstream[]>;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,57 +54,44 @@ export class Item extends DSpaceObject {
|
|||||||
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
||||||
*/
|
*/
|
||||||
getThumbnail(): Observable<Bitstream> {
|
getThumbnail(): Observable<Bitstream> {
|
||||||
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
|
//TODO currently this just picks the first thumbnail
|
||||||
return bundle
|
//should be adjusted when we have a way to determine
|
||||||
.filter(bundle => hasValue(bundle))
|
//the primary thumbnail from rest
|
||||||
.flatMap(bundle => bundle.primaryBitstream.payload)
|
return this.getBitstreamsByBundleName("THUMBNAIL")
|
||||||
.startWith(undefined);
|
.filter(thumbnails => isNotEmpty(thumbnails))
|
||||||
|
.map(thumbnails => thumbnails[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the thumbnail for the given original of this item
|
* Retrieves the thumbnail for the given original of this item
|
||||||
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
||||||
*/
|
*/
|
||||||
getThumbnailForOriginal(original: Bitstream): Observable<Bitstream> {
|
getThumbnailForOriginal(original: Bitstream): Observable<Bitstream> {
|
||||||
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
|
return this.getBitstreamsByBundleName("THUMBNAIL").map(files => files
|
||||||
return bundle
|
.find(thumbnail => thumbnail
|
||||||
.filter(bundle => hasValue(bundle))
|
.name.startsWith(original.name)
|
||||||
.flatMap(bundle => bundle
|
)
|
||||||
.bitstreams.payload.map(files => files
|
).startWith(undefined);
|
||||||
.find(thumbnail => thumbnail
|
}
|
||||||
.name.startsWith(original.name)
|
|
||||||
)
|
/**
|
||||||
)
|
* Retrieves all files that should be displayed on the item page of this item
|
||||||
)
|
* @returns {Observable<Array<Observable<Bitstream>>>} an array of all Bitstreams in the "ORIGINAL" bundle
|
||||||
.startWith(undefined);;
|
*/
|
||||||
|
getFiles(): Observable<Bitstream[]> {
|
||||||
|
return this.getBitstreamsByBundleName("ORIGINAL");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all files that should be displayed on the item page of this item
|
* Retrieves bitstreams by bundle name
|
||||||
* @returns {Observable<Array<Observable<Bitstream>>>} an array of all Bitstreams in the "ORIGINAL" bundle
|
* @param bundleName The name of the Bundle that should be returned
|
||||||
*/
|
* @returns {Observable<Bitstream[]>} the bitstreams with the given bundleName
|
||||||
getFiles(name: String = "ORIGINAL"): Observable<Bitstream[]> {
|
*/
|
||||||
const bundle: Observable <Bundle> = this.getBundle(name);
|
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||||
return bundle
|
return this.bitstreams.payload.startWith([])
|
||||||
.filter(bundle => hasValue(bundle))
|
.map(bitstreams => bitstreams
|
||||||
.flatMap(bundle => bundle.bitstreams.payload)
|
.filter(bitstream => bitstream.bundleName === bundleName)
|
||||||
.startWith([]);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the bundle of this item by its name
|
|
||||||
* @param name The name of the Bundle that should be returned
|
|
||||||
* @returns {Observable<Bundle>} the Bundle that belongs to this item with the given name
|
|
||||||
*/
|
|
||||||
getBundle(name: String): Observable<Bundle> {
|
|
||||||
return this.bundles.payload
|
|
||||||
.filter(bundles => hasValue(bundles))
|
|
||||||
.map(bundles => {
|
|
||||||
return bundles.find((bundle: Bundle) => {
|
|
||||||
return bundle.name === name
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.startWith(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
30
src/app/core/shared/page-info.model.ts
Normal file
30
src/app/core/shared/page-info.model.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from "cerialize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of a paginated response
|
||||||
|
*/
|
||||||
|
export class PageInfo {
|
||||||
|
/**
|
||||||
|
* The number of elements on a page
|
||||||
|
*/
|
||||||
|
@autoserializeAs(Number, 'size')
|
||||||
|
elementsPerPage: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of elements in the entire set
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
totalElements: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of pages
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
totalPages: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of the current page, zero-based
|
||||||
|
*/
|
||||||
|
@autoserializeAs(Number, 'number')
|
||||||
|
currentPage: number;
|
||||||
|
}
|
@@ -5,6 +5,7 @@
|
|||||||
export enum ResourceType {
|
export enum ResourceType {
|
||||||
Bundle = <any> "bundle",
|
Bundle = <any> "bundle",
|
||||||
Bitstream = <any> "bitstream",
|
Bitstream = <any> "bitstream",
|
||||||
|
BitstreamFormat = <any> "bitstreamformat",
|
||||||
Item = <any> "item",
|
Item = <any> "item",
|
||||||
Collection = <any> "collection",
|
Collection = <any> "collection",
|
||||||
Community = <any> "community"
|
Community = <any> "community"
|
||||||
|
@@ -7,12 +7,14 @@ import { TopLevelCommunityListComponent } from "./top-level-community-list/top-l
|
|||||||
import { HomeNewsComponent } from "./home-news/home-news.component";
|
import { HomeNewsComponent } from "./home-news/home-news.component";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from "@ngx-translate/core";
|
||||||
|
import { SharedModule } from "../shared/shared.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
HomeRoutingModule,
|
HomeRoutingModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
SharedModule,
|
||||||
TranslateModule
|
TranslateModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
<div *ngIf="topLevelCommunities.hasSucceeded | async">
|
<div *ngIf="topLevelCommunities.hasSucceeded | async">
|
||||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||||
<ul>
|
<ds-object-list [config]="config" [sortConfig]="sortConfig"
|
||||||
<li *ngFor="let community of (topLevelCommunities.payload | async)">
|
[objects]="topLevelCommunities" [hideGear]="true"
|
||||||
<p>
|
(pageChange)="onPageChange($event)"
|
||||||
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
<span class="text-muted">{{community.shortDescription}}</span>
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
</p>
|
(sortFieldChange)="onSortDirectionChange($event)"></ds-object-list>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,18 +1,24 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { CommunityDataService } from "../../core/data/community-data.service";
|
|
||||||
import { RemoteData } from "../../core/data/remote-data";
|
import { RemoteData } from "../../core/data/remote-data";
|
||||||
|
import { CommunityDataService } from "../../core/data/community-data.service";
|
||||||
import { Community } from "../../core/shared/community.model";
|
import { Community } from "../../core/shared/community.model";
|
||||||
|
import { PaginationComponentOptions } from "../../shared/pagination/pagination-component-options.model";
|
||||||
|
import { SortOptions, SortDirection } from "../../core/cache/models/sort-options.model";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-top-level-community-list',
|
selector: 'ds-top-level-community-list',
|
||||||
styleUrls: ['./top-level-community-list.component.css'],
|
styleUrls: ['./top-level-community-list.component.css'],
|
||||||
templateUrl: './top-level-community-list.component.html'
|
templateUrl: './top-level-community-list.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class TopLevelCommunityListComponent implements OnInit {
|
export class TopLevelCommunityListComponent implements OnInit {
|
||||||
topLevelCommunities: RemoteData<Community[]>;
|
topLevelCommunities: RemoteData<Community[]>;
|
||||||
|
config : PaginationComponentOptions;
|
||||||
|
sortConfig : SortOptions;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cds: CommunityDataService
|
private cds: CommunityDataService,
|
||||||
|
private ref: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.universalInit();
|
this.universalInit();
|
||||||
}
|
}
|
||||||
@@ -22,6 +28,38 @@ export class TopLevelCommunityListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.topLevelCommunities = this.cds.findAll();
|
this.config = new PaginationComponentOptions();
|
||||||
|
this.config.id = "top-level-pagination";
|
||||||
|
this.config.pageSizeOptions = [ 4 ];
|
||||||
|
this.config.pageSize = 4;
|
||||||
|
this.sortConfig = new SortOptions();
|
||||||
|
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(currentPage: number): void {
|
||||||
|
this.config.currentPage = currentPage;
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange(elementsPerPage: number): void {
|
||||||
|
this.config.pageSize = elementsPerPage;
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortDirectionChange(sortDirection: SortDirection): void {
|
||||||
|
this.sortConfig = new SortOptions(this.sortConfig.field, sortDirection);
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortFieldChange(field: string): void {
|
||||||
|
this.sortConfig = new SortOptions(field, this.sortConfig.direction);
|
||||||
|
this.updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResults() {
|
||||||
|
this.topLevelCommunities = undefined;
|
||||||
|
this.topLevelCommunities = this.cds.findAll({ currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, sort: this.sortConfig });
|
||||||
|
// this.ref.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
<ds-metadata-field-wrapper [label]="label | translate">
|
||||||
<div class="collections">
|
<div class="collections">
|
||||||
<a *ngFor="let collection of (collections | async); let last=last;" [href]="collection?.self">
|
<a *ngFor="let collection of (collections | async); let last=last;" [routerLink]="['/collections', collection.id]">
|
||||||
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
|
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||||||
import { Collection } from "../../../core/shared/collection.model";
|
import { Collection } from "../../../core/shared/collection.model";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { Item } from "../../../core/shared/item.model";
|
import { Item } from "../../../core/shared/item.model";
|
||||||
|
import { RemoteDataBuildService } from "../../../core/cache/builders/remote-data-build.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the parent collections section of the item
|
* This component renders the parent collections section of the item
|
||||||
@@ -18,11 +19,13 @@ export class CollectionsComponent implements OnInit {
|
|||||||
|
|
||||||
label : string = "item.page.collections";
|
label : string = "item.page.collections";
|
||||||
|
|
||||||
separator: string = "<br/>"
|
separator: string = "<br/>";
|
||||||
|
|
||||||
collections: Observable<Collection[]>;
|
collections: Observable<Collection[]>;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
|
private rdbs: RemoteDataBuildService
|
||||||
|
) {
|
||||||
this.universalInit();
|
this.universalInit();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -31,7 +34,11 @@ export class CollectionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collections = this.item.parents.payload;
|
// this.collections = this.item.parents.payload;
|
||||||
|
//TODO this should use parents, but the collections
|
||||||
|
// for an Item aren't returned by the REST API yet,
|
||||||
|
// only the owning collection
|
||||||
|
this.collections = this.item.owner.payload.map(c => [c]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<dd class="col-md-8">{{file.name}}</dd>
|
<dd class="col-md-8">{{file.name}}</dd>
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||||
<dd class="col-md-8">{{(file.size) | dsFileSize }}</dd>
|
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<a [href]="file.retrieve">
|
<a [href]="file.retrieve" [download]="file.name">
|
||||||
{{"item.page.filesection.download" | translate}}
|
{{"item.page.filesection.download" | translate}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -35,8 +35,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
const originals = this.item.getFiles("ORIGINAL");
|
const originals = this.item.getFiles();
|
||||||
const licenses = this.item.getFiles("LICENSE");
|
const licenses = this.item.getBitstreamsByBundleName("LICENSE");
|
||||||
this.files = Observable.combineLatest(originals, licenses, (originals, licenses) => [...originals, ...licenses]);
|
this.files = Observable.combineLatest(originals, licenses, (originals, licenses) => [...originals, ...licenses]);
|
||||||
this.files.subscribe(
|
this.files.subscribe(
|
||||||
files =>
|
files =>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
<ds-metadata-field-wrapper [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.retrieve">
|
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.retrieve" [download]="file?.name">
|
||||||
<span>{{file?.name}}</span>
|
<span>{{file?.name}}</span>
|
||||||
<span>({{(file?.size) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a [routerLink]="['/collections/' + collection.id]" class="lead">
|
||||||
|
{{collection.name}}
|
||||||
|
</a>
|
||||||
|
<div *ngIf="collection.shortDescription" class="text-muted">
|
||||||
|
{{collection.shortDescription}}
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Collection } from "../../core/shared/collection.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-list-element',
|
||||||
|
styleUrls: ['./collection-list-element.component.css'],
|
||||||
|
templateUrl: './collection-list-element.component.html'
|
||||||
|
})
|
||||||
|
export class CollectionListElementComponent {
|
||||||
|
|
||||||
|
@Input() collection: Collection;
|
||||||
|
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.universalInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
universalInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a [routerLink]="['/communities/' + community.id]" class="lead">
|
||||||
|
{{community.name}}
|
||||||
|
</a>
|
||||||
|
<div *ngIf="community.shortDescription" class="text-muted">
|
||||||
|
{{community.shortDescription}}
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Community } from "../../core/shared/community.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-list-element',
|
||||||
|
styleUrls: ['./community-list-element.component.css'],
|
||||||
|
templateUrl: './community-list-element.component.html'
|
||||||
|
})
|
||||||
|
export class CommunityListElementComponent {
|
||||||
|
|
||||||
|
@Input() community: Community;
|
||||||
|
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.universalInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
universalInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
<a [routerLink]="['/items/' + item.id]" class="lead">
|
||||||
|
{{item.findMetadata("dc.title")}}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted">
|
||||||
|
<span *ngIf="item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
|
||||||
|
<span *ngFor="let authorMd of item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||||
|
<span *ngIf="!last">; </span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
(<span *ngIf="item.findMetadata('dc.publisher')" class="item-list-publisher">{{item.findMetadata("dc.publisher")}}, </span><span *ngIf="item.findMetadata('dc.date.issued')" class="item-list-date">{{item.findMetadata("dc.date.issued")}}</span>)
|
||||||
|
</span>
|
||||||
|
<div *ngIf="item.findMetadata('dc.description.abstract')" class="item-list-abstract">{{item.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Item } from "../../core/shared/item.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-list-element',
|
||||||
|
styleUrls: ['./item-list-element.component.css'],
|
||||||
|
templateUrl: './item-list-element.component.html'
|
||||||
|
})
|
||||||
|
export class ItemListElementComponent {
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.universalInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
universalInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
<div [ngSwitch]="object.type">
|
||||||
|
<ds-item-list-element *ngSwitchCase="type.Item" [item]="object"></ds-item-list-element>
|
||||||
|
<ds-collection-list-element *ngSwitchCase="type.Collection" [collection]="object"></ds-collection-list-element>
|
||||||
|
<ds-community-list-element *ngSwitchCase="type.Community" [community]="object"></ds-community-list-element>
|
||||||
|
</div>
|
@@ -0,0 +1,7 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../../node_modules/bootstrap/scss/variables';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: $spacer-y;
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { DSpaceObject } from "../../core/shared/dspace-object.model";
|
||||||
|
import { ResourceType } from "../../core/shared/resource-type";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-object-list-element',
|
||||||
|
styleUrls: ['./object-list-element.component.css'],
|
||||||
|
templateUrl: './object-list-element.component.html'
|
||||||
|
})
|
||||||
|
export class ObjectListElementComponent {
|
||||||
|
|
||||||
|
public type = ResourceType;
|
||||||
|
|
||||||
|
@Input() object: DSpaceObject;
|
||||||
|
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.universalInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
universalInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
src/app/object-list/object-list.component.html
Normal file
16
src/app/object-list/object-list.component.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<ds-pagination [paginationOptions]="config"
|
||||||
|
[collectionSize]="(pageInfo | async)?.totalElements"
|
||||||
|
[sortOptions]="sortConfig"
|
||||||
|
[hideGear]="hideGear"
|
||||||
|
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortDirectionChange($event)">
|
||||||
|
<ul *ngIf="objects.hasSucceeded | async"> <!--class="list-unstyled"-->
|
||||||
|
<li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }">
|
||||||
|
<ds-object-list-element [object]="object"></ds-object-list-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</ds-pagination>
|
1
src/app/object-list/object-list.component.scss
Normal file
1
src/app/object-list/object-list.component.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '../../styles/variables.scss';
|
@@ -1,3 +1,3 @@
|
|||||||
<div *ngIf="logo" class="dso-logo">
|
<div *ngIf="logo" class="dso-logo">
|
||||||
<img [src]="logo.url" class="img-responsive" [attr.alt]="alternateText ? alternateText : null" />
|
<img [src]="logo.retrieve" class="img-responsive" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,4 +12,14 @@ export class ComcolPageLogoComponent {
|
|||||||
@Input() logo: Bitstream;
|
@Input() logo: Bitstream;
|
||||||
|
|
||||||
@Input() alternateText: string;
|
@Input() alternateText: string;
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* The default 'holder.js' image
|
||||||
|
*/
|
||||||
|
holderSource: string = "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E";
|
||||||
|
|
||||||
|
|
||||||
|
errorHandler(event) {
|
||||||
|
event.currentTarget.src = this.holderSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
82
src/app/shared/object-list/object-list.component.ts
Normal file
82
src/app/shared/object-list/object-list.component.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Component, Input, ViewEncapsulation, ChangeDetectionStrategy,
|
||||||
|
OnInit, Output
|
||||||
|
} from '@angular/core';
|
||||||
|
import { RemoteData } from "../../core/data/remote-data";
|
||||||
|
import { DSpaceObject } from "../../core/shared/dspace-object.model";
|
||||||
|
import { PageInfo } from "../../core/shared/page-info.model";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { PaginationComponentOptions } from "../pagination/pagination-component-options.model";
|
||||||
|
import { EventEmitter } from "@angular/common/src/facade/async";
|
||||||
|
import { SortOptions, SortDirection } from "../../core/cache/models/sort-options.model";
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
|
encapsulation: ViewEncapsulation.Emulated,
|
||||||
|
selector: 'ds-object-list',
|
||||||
|
styleUrls: ['../../object-list/object-list.component.css'],
|
||||||
|
templateUrl: '../../object-list/object-list.component.html'
|
||||||
|
})
|
||||||
|
export class ObjectListComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() objects: RemoteData<DSpaceObject[]>;
|
||||||
|
@Input() config : PaginationComponentOptions;
|
||||||
|
@Input() sortConfig : SortOptions;
|
||||||
|
@Input() hideGear : boolean = false;
|
||||||
|
@Input() hidePagerWhenSinglePage : boolean = true;
|
||||||
|
pageInfo : Observable<PageInfo>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page is changed.
|
||||||
|
* Event's payload equals to the newly selected page.
|
||||||
|
*/
|
||||||
|
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page wsize is changed.
|
||||||
|
* Event's payload equals to the newly selected page size.
|
||||||
|
*/
|
||||||
|
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort direction is changed.
|
||||||
|
* Event's payload equals to the newly selected sort direction.
|
||||||
|
*/
|
||||||
|
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort field is changed.
|
||||||
|
* Event's payload equals to the newly selected sort field.
|
||||||
|
*/
|
||||||
|
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.universalInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
universalInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.pageInfo = this.objects.pageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(event) {
|
||||||
|
this.pageChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange(event) {
|
||||||
|
this.pageSizeChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortDirectionChange(event) {
|
||||||
|
this.sortDirectionChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortFieldChange(event) {
|
||||||
|
this.sortFieldChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
export class PaginationOptions extends NgbPaginationConfig {
|
export class PaginationComponentOptions extends NgbPaginationConfig {
|
||||||
/**
|
/**
|
||||||
* ID for the pagination instance. Only useful if you wish to
|
* ID for the pagination instance. Only useful if you wish to
|
||||||
* have more than once instance at a time in a given component.
|
* have more than once instance at a time in a given component.
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="pagination-masked clearfix top">
|
<div *ngIf="!hideGear" class="pagination-masked clearfix top">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col pagination-info">
|
<div class="col pagination-info">
|
||||||
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
<button class="btn btn-outline-primary" id="paginationControls" (click)="$event.stopPropagation(); (paginationControls.isOpen())?paginationControls.close():paginationControls.open();"><i class="fa fa-cog" aria-hidden="true"></i></button>
|
<button class="btn btn-outline-primary" id="paginationControls" (click)="$event.stopPropagation(); (paginationControls.isOpen())?paginationControls.close():paginationControls.open();"><i class="fa fa-cog" aria-hidden="true"></i></button>
|
||||||
<div class="dropdown-menu dropdown-menu-right" id="paginationControlsDropdownMenu" aria-labelledby="paginationControls">
|
<div class="dropdown-menu dropdown-menu-right" id="paginationControlsDropdownMenu" aria-labelledby="paginationControls">
|
||||||
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
||||||
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let item of pageSizeOptions " (click)="setPageSize(item)"><i class="fa fa-check {{(item != paginationOptions.pageSize) ? 'invisible' : ''}}" aria-hidden="true"></i> {{item}} </button>
|
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let item of pageSizeOptions" (click)="setPageSize(item)"><i class="fa fa-check {{(item != paginationOptions.pageSize) ? 'invisible' : ''}}" aria-hidden="true"></i> {{item}} </button>
|
||||||
|
<h6 class="dropdown-header">{{ 'pagination.sort-direction' | translate}}</h6>
|
||||||
|
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let direction of (sortDirections | dsKeys)" (click)="setSortDirection(direction.key)"><i class="fa fa-check {{(direction.key != sortOptions.direction) ? 'invisible' : ''}}" aria-hidden="true"></i> {{direction.value}} </button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +20,7 @@
|
|||||||
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|
||||||
<div class="pagination justify-content-center clearfix bottom">
|
<div *ngIf="shouldShowBottomPager" class="pagination justify-content-center clearfix bottom">
|
||||||
<ngb-pagination [boundaryLinks]="paginationOptions.boundaryLinks"
|
<ngb-pagination [boundaryLinks]="paginationOptions.boundaryLinks"
|
||||||
[collectionSize]="collectionSize"
|
[collectionSize]="collectionSize"
|
||||||
[disabled]="paginationOptions.disabled"
|
[disabled]="paginationOptions.disabled"
|
||||||
|
@@ -25,12 +25,14 @@ import { Ng2PaginationModule } from 'ng2-pagination';
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { PaginationComponent } from './pagination.component';
|
import { PaginationComponent } from './pagination.component';
|
||||||
import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
|
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||||
import { MockTranslateLoader } from "../testing/mock-translate-loader";
|
import { MockTranslateLoader } from "../testing/mock-translate-loader";
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, EnvConfig } from '../../../config';
|
import { GLOBAL_CONFIG, EnvConfig } from '../../../config';
|
||||||
import { ActivatedRouteStub, RouterStub } from "../testing/router-stubs";
|
import { ActivatedRouteStub, RouterStub } from "../testing/router-stubs";
|
||||||
import { HostWindowService } from "../host-window.service";
|
import { HostWindowService } from "../host-window.service";
|
||||||
|
import { EnumKeysPipe } from "../utils/enum-keys-pipe";
|
||||||
|
import { SortOptions } from "../../core/cache/models/sort-options.model";
|
||||||
|
|
||||||
|
|
||||||
function createTestComponent<T>(html: string, type: {new (...args: any[]): T}): ComponentFixture<T> {
|
function createTestComponent<T>(html: string, type: {new (...args: any[]): T}): ComponentFixture<T> {
|
||||||
@@ -138,7 +140,7 @@ describe('Pagination component', () => {
|
|||||||
RouterTestingModule.withRoutes([
|
RouterTestingModule.withRoutes([
|
||||||
{path: 'home', component: TestComponent}
|
{path: 'home', component: TestComponent}
|
||||||
])],
|
])],
|
||||||
declarations: [PaginationComponent, TestComponent], // declare the test component
|
declarations: [PaginationComponent, TestComponent, EnumKeysPipe], // declare the test component
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
|
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
|
||||||
@@ -156,6 +158,7 @@ describe('Pagination component', () => {
|
|||||||
html = `
|
html = `
|
||||||
<ds-pagination #p="paginationComponent"
|
<ds-pagination #p="paginationComponent"
|
||||||
[paginationOptions]="paginationOptions"
|
[paginationOptions]="paginationOptions"
|
||||||
|
[sortOptions]="sortOptions"
|
||||||
[collectionSize]="collectionSize"
|
[collectionSize]="collectionSize"
|
||||||
(pageChange)="pageChanged($event)"
|
(pageChange)="pageChanged($event)"
|
||||||
(pageSizeChange)="pageSizeChanged($event)">
|
(pageSizeChange)="pageSizeChanged($event)">
|
||||||
@@ -247,12 +250,12 @@ describe('Pagination component', () => {
|
|||||||
|
|
||||||
changePage(testFixture, 3);
|
changePage(testFixture, 3);
|
||||||
tick();
|
tick();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10 } });
|
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 0, sortField: 'name' } });
|
||||||
expect(paginationComponent.currentPage).toEqual(3);
|
expect(paginationComponent.currentPage).toEqual(3);
|
||||||
|
|
||||||
changePageSize(testFixture, '20');
|
changePageSize(testFixture, '20');
|
||||||
tick();
|
tick();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 20 } });
|
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 20, sortDirection: 0, sortField: 'name' } });
|
||||||
expect(paginationComponent.pageSize).toEqual(20);
|
expect(paginationComponent.pageSize).toEqual(20);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -307,7 +310,8 @@ class TestComponent {
|
|||||||
|
|
||||||
collection: string[] = [];
|
collection: string[] = [];
|
||||||
collectionSize: number;
|
collectionSize: number;
|
||||||
paginationOptions = new PaginationOptions();
|
paginationOptions = new PaginationComponentOptions();
|
||||||
|
sortOptions = new SortOptions();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.collection = Array.from(new Array(100), (x, i) => `item ${i + 1}`);
|
this.collection = Array.from(new Array(100), (x, i) => `item ${i + 1}`);
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Subscription } from "rxjs/Subscription";
|
||||||
import { isNumeric } from "rxjs/util/isNumeric";
|
import { isNumeric } from "rxjs/util/isNumeric";
|
||||||
import 'rxjs/add/operator/switchMap';
|
import 'rxjs/add/operator/switchMap';
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
@@ -17,231 +18,338 @@ import { DEFAULT_TEMPLATE, DEFAULT_STYLES } from 'ng2-pagination/dist/template';
|
|||||||
|
|
||||||
import { HostWindowService } from "../host-window.service";
|
import { HostWindowService } from "../host-window.service";
|
||||||
import { HostWindowState } from "../host-window.reducer";
|
import { HostWindowState } from "../host-window.reducer";
|
||||||
import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
|
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from "../../core/cache/models/sort-options.model";
|
||||||
|
import { hasValue } from "../empty.util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default pagination controls component.
|
* The default pagination controls component.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
exportAs: 'paginationComponent',
|
exportAs: 'paginationComponent',
|
||||||
selector: 'ds-pagination',
|
selector: 'ds-pagination',
|
||||||
templateUrl: 'pagination.component.html',
|
templateUrl: 'pagination.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.Default,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
encapsulation: ViewEncapsulation.Emulated
|
encapsulation: ViewEncapsulation.Emulated
|
||||||
})
|
})
|
||||||
export class PaginationComponent implements OnDestroy, OnInit {
|
export class PaginationComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of items in collection.
|
* Number of items in collection.
|
||||||
*/
|
*/
|
||||||
@Input() collectionSize: number;
|
@Input() collectionSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the NgbPagination component.
|
||||||
|
*/
|
||||||
|
@Input() paginationOptions: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort configuration for this component.
|
||||||
|
*/
|
||||||
|
@Input() sortOptions: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page is changed.
|
||||||
|
* Event's payload equals to the newly selected page.
|
||||||
|
*/
|
||||||
|
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page wsize is changed.
|
||||||
|
* Event's payload equals to the newly selected page size.
|
||||||
|
*/
|
||||||
|
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort direction is changed.
|
||||||
|
* Event's payload equals to the newly selected sort direction.
|
||||||
|
*/
|
||||||
|
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort field is changed.
|
||||||
|
* Event's payload equals to the newly selected sort field.
|
||||||
|
*/
|
||||||
|
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the gear
|
||||||
|
*/
|
||||||
|
@Input() public hideGear: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pager when there is less than 2 pages
|
||||||
|
*/
|
||||||
|
@Input() public hidePagerWhenSinglePage: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current page.
|
||||||
|
*/
|
||||||
|
public currentPage = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current URL query parameters
|
||||||
|
*/
|
||||||
|
public currentQueryParams = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable of HostWindowState type
|
||||||
|
*/
|
||||||
|
public hostWindow: Observable<HostWindowState>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID for the pagination instance. Only useful if you wish to
|
||||||
|
* have more than once instance at a time in a given component.
|
||||||
|
*/
|
||||||
|
private id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean that indicate if is an extra small devices viewport.
|
||||||
|
*/
|
||||||
|
public isXs: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of items per page.
|
||||||
|
*/
|
||||||
|
public pageSize: number = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare SortDirection enumeration to use it in the template
|
||||||
|
*/
|
||||||
|
public sortDirections = SortDirection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number array that represents options for a context pagination limit.
|
||||||
|
*/
|
||||||
|
private pageSizeOptions: Array<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direction in which to sort: ascending or descending
|
||||||
|
*/
|
||||||
|
public sortDirection: SortDirection = SortDirection.Ascending;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the field that's used to sort by
|
||||||
|
*/
|
||||||
|
public sortField: string = "id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local variable, which can be used in the template to access the paginate controls ngbDropdown methods and properties
|
||||||
|
*/
|
||||||
|
public paginationControls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the NgbPagination component.
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
*/
|
*/
|
||||||
@Input() paginationOptions: PaginationOptions;
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event fired when the page is changed.
|
* An object that represents pagination details of the current viewed page
|
||||||
* Event's payload equals to the newly selected page.
|
*/
|
||||||
*/
|
public showingDetail: any = {
|
||||||
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
range: null,
|
||||||
|
total: null
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event fired when the page size is changed.
|
* Method provided by Angular. Invoked after the constructor.
|
||||||
* Event's payload equals to the newly selected page size.
|
*/
|
||||||
*/
|
ngOnInit() {
|
||||||
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
this.subs.push(this.hostWindowService.isXs()
|
||||||
|
.subscribe((status: boolean) => {
|
||||||
|
this.isXs = status;
|
||||||
|
}));
|
||||||
|
this.checkConfig(this.paginationOptions);
|
||||||
|
this.id = this.paginationOptions.id || null;
|
||||||
|
this.currentPage = this.paginationOptions.currentPage;
|
||||||
|
this.pageSize = this.paginationOptions.pageSize;
|
||||||
|
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
|
||||||
|
this.sortDirection = this.sortOptions.direction;
|
||||||
|
this.sortField = this.sortOptions.field;
|
||||||
|
this.subs.push(this.route.queryParams
|
||||||
|
.filter(queryParams => hasValue(queryParams))
|
||||||
|
.subscribe(queryParams => {
|
||||||
|
this.currentQueryParams = queryParams;
|
||||||
|
if (this.id == queryParams['pageId']
|
||||||
|
&& (this.paginationOptions.currentPage != queryParams['page']
|
||||||
|
|| this.paginationOptions.pageSize != queryParams['pageSize']
|
||||||
|
|| this.sortOptions.direction != queryParams['sortDirection']
|
||||||
|
|| this.sortOptions.field != queryParams['sortField'] )
|
||||||
|
) {
|
||||||
|
this.validateParams(queryParams['page'], queryParams['pageSize'], queryParams['sortDirection'], queryParams['sortField']);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.setShowingDetail();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current page.
|
* Method provided by Angular. Invoked when the instance is destroyed.
|
||||||
*/
|
*/
|
||||||
public currentPage = 1;
|
ngOnDestroy() {
|
||||||
|
this.subs
|
||||||
/**
|
.filter(sub => hasValue(sub))
|
||||||
* Current URL query parameters
|
.forEach(sub => sub.unsubscribe());
|
||||||
*/
|
}
|
||||||
public currentQueryParams = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable of HostWindowState type
|
* @param route
|
||||||
*/
|
* Route is a singleton service provided by Angular.
|
||||||
public hostWindow: Observable<HostWindowState>;
|
* @param router
|
||||||
|
* Router is a singleton service provided by Angular.
|
||||||
|
*/
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
public hostWindowService: HostWindowService) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID for the pagination instance. Only useful if you wish to
|
* Method to set set new page and update route parameters
|
||||||
* have more than once instance at a time in a given component.
|
*
|
||||||
*/
|
* @param page
|
||||||
private id: string;
|
* The page being navigated to.
|
||||||
|
*/
|
||||||
|
public doPageChange(page: number) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.updateRoute();
|
||||||
|
this.setShowingDetail();
|
||||||
|
this.pageChange.emit(page);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean that indicate if is an extra small devices viewport.
|
* Method to set set new page size and update route parameters
|
||||||
*/
|
*
|
||||||
public isXs: boolean;
|
* @param pageSize
|
||||||
|
* The new page size.
|
||||||
|
*/
|
||||||
|
public setPageSize(pageSize: number) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.updateRoute();
|
||||||
|
this.setShowingDetail();
|
||||||
|
this.pageSizeChange.emit(pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of items per page.
|
* Method to set set new sort direction and update route parameters
|
||||||
*/
|
*
|
||||||
public pageSize: number = 10;
|
* @param sortDirection
|
||||||
|
* The new sort direction.
|
||||||
|
*/
|
||||||
|
public setSortDirection(sortDirection: SortDirection) {
|
||||||
|
this.sortDirection = sortDirection;
|
||||||
|
this.updateRoute();
|
||||||
|
this.setShowingDetail();
|
||||||
|
this.sortDirectionChange.emit(sortDirection);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A number array that represents options for a context pagination limit.
|
* Method to set set new sort field and update route parameters
|
||||||
*/
|
*
|
||||||
private pageSizeOptions: Array<number>;
|
* @param sortField
|
||||||
|
* The new sort field.
|
||||||
|
*/
|
||||||
|
public setSortField(field: string) {
|
||||||
|
this.sortField = field;
|
||||||
|
this.updateRoute();
|
||||||
|
this.setShowingDetail();
|
||||||
|
this.sortFieldChange.emit(field);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local variable, which can be used in the template to access the paginate controls ngbDropdown methods and properties
|
* Method to update the route parameters
|
||||||
*/
|
*/
|
||||||
public paginationControls;
|
private updateRoute() {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: Object.assign({}, this.currentQueryParams, {
|
||||||
|
pageId: this.id,
|
||||||
|
page: this.currentPage,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
sortDirection: this.sortDirection,
|
||||||
|
sortField: this.sortField
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscriber to observable.
|
* Method to set pagination details of the current viewed page.
|
||||||
*/
|
*/
|
||||||
private routeSubscription: any;
|
private setShowingDetail() {
|
||||||
|
let firstItem;
|
||||||
|
let lastItem;
|
||||||
|
let lastPage = Math.round(this.collectionSize / this.pageSize);
|
||||||
|
|
||||||
/**
|
firstItem = this.pageSize * (this.currentPage - 1) + 1;
|
||||||
* An object that represents pagination details of the current viewed page
|
if (this.currentPage != lastPage) {
|
||||||
*/
|
lastItem = this.pageSize * this.currentPage;
|
||||||
public showingDetail: any = {
|
} else {
|
||||||
range: null,
|
lastItem = this.collectionSize;
|
||||||
total: null
|
}
|
||||||
};
|
this.showingDetail = {
|
||||||
|
range: firstItem + ' - ' + lastItem,
|
||||||
/**
|
total: this.collectionSize
|
||||||
* Subscriber to observable.
|
|
||||||
*/
|
|
||||||
private stateSubscription: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method provided by Angular. Invoked after the constructor.
|
|
||||||
*/
|
|
||||||
ngOnInit() {
|
|
||||||
this.stateSubscription = this.hostWindowService.isXs()
|
|
||||||
.subscribe((status: boolean) => {
|
|
||||||
this.isXs = status;
|
|
||||||
});
|
|
||||||
this.checkConfig(this.paginationOptions);
|
|
||||||
this.id = this.paginationOptions.id || null;
|
|
||||||
this.currentPage = this.paginationOptions.currentPage;
|
|
||||||
this.pageSize = this.paginationOptions.pageSize;
|
|
||||||
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
|
|
||||||
|
|
||||||
this.routeSubscription = this.route.queryParams
|
|
||||||
.map(queryParams => queryParams)
|
|
||||||
.subscribe(queryParams => {
|
|
||||||
this.currentQueryParams = queryParams;
|
|
||||||
if(this.id == queryParams['pageId']
|
|
||||||
&& (this.paginationOptions.currentPage != queryParams['page']
|
|
||||||
|| this.paginationOptions.pageSize != queryParams['pageSize'])
|
|
||||||
) {
|
|
||||||
this.validateParams(queryParams['page'], queryParams['pageSize']);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
this.setShowingDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method provided by Angular. Invoked when the instance is destroyed.
|
|
||||||
*/
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.stateSubscription.unsubscribe();
|
|
||||||
this.routeSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param route
|
|
||||||
* Route is a singleton service provided by Angular.
|
|
||||||
* @param router
|
|
||||||
* Router is a singleton service provided by Angular.
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
public hostWindowService: HostWindowService
|
|
||||||
){
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to set set new page and update route parameters
|
|
||||||
*
|
|
||||||
* @param page
|
|
||||||
* The page being navigated to.
|
|
||||||
*/
|
|
||||||
public doPageChange(page: number) {
|
|
||||||
this.router.navigate([], { queryParams: Object.assign({}, this.currentQueryParams, { pageId: this.id, page: page, pageSize: this.pageSize }) });
|
|
||||||
this.currentPage = page;
|
|
||||||
this.setShowingDetail();
|
|
||||||
this.pageChange.emit(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to set set new page size and update route parameters
|
|
||||||
*
|
|
||||||
* @param pageSize
|
|
||||||
* The new page size.
|
|
||||||
*/
|
|
||||||
public setPageSize(pageSize: number) {
|
|
||||||
this.router.navigate([], { queryParams: Object.assign({}, this.currentQueryParams, { pageId: this.id, page: this.currentPage, pageSize: pageSize }) });
|
|
||||||
this.pageSize = pageSize;
|
|
||||||
this.setShowingDetail();
|
|
||||||
this.pageSizeChange.emit(pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to set pagination details of the current viewed page.
|
|
||||||
*/
|
|
||||||
private setShowingDetail() {
|
|
||||||
let firstItem;
|
|
||||||
let lastItem;
|
|
||||||
let lastPage = Math.round(this.collectionSize / this.pageSize);
|
|
||||||
|
|
||||||
firstItem = this.pageSize * (this.currentPage - 1) + 1;
|
|
||||||
if (this.currentPage != lastPage) {
|
|
||||||
lastItem = this.pageSize * this.currentPage;
|
|
||||||
} else {
|
|
||||||
lastItem = this.collectionSize;
|
|
||||||
}
|
}
|
||||||
this.showingDetail = {
|
|
||||||
range: firstItem + ' - ' + lastItem,
|
|
||||||
total: this.collectionSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate query params
|
* Validate query params
|
||||||
*
|
*
|
||||||
* @param page
|
* @param page
|
||||||
* The page number to validate
|
* The page number to validate
|
||||||
* @param pageSize
|
* @param pageSize
|
||||||
* The page size to validate
|
* The page size to validate
|
||||||
*/
|
*/
|
||||||
private validateParams(page: any, pageSize: any) {
|
private validateParams(page: any, pageSize: any, sortDirection: any, sortField: any) {
|
||||||
let filteredPageSize = this.pageSizeOptions.find(x => x == pageSize);
|
let filteredPageSize = this.pageSizeOptions.find(x => x == pageSize);
|
||||||
if (!isNumeric(page) || !filteredPageSize) {
|
if (!isNumeric(page) || !filteredPageSize) {
|
||||||
let filteredPage = isNumeric(page) ? page : this.currentPage;
|
let filteredPage = isNumeric(page) ? page : this.currentPage;
|
||||||
filteredPageSize = (filteredPageSize) ? filteredPageSize : this.pageSize;
|
filteredPageSize = (filteredPageSize) ? filteredPageSize : this.pageSize;
|
||||||
this.router.navigate([{ pageId: this.id, page: filteredPage, pageSize: filteredPageSize }]);
|
this.router.navigate([], {
|
||||||
} else {
|
queryParams: {
|
||||||
// (+) converts string to a number
|
pageId: this.id,
|
||||||
this.currentPage = +page;
|
page: filteredPage,
|
||||||
this.pageSize = +pageSize;
|
pageSize: filteredPageSize,
|
||||||
this.pageChange.emit(this.currentPage);
|
sortDirection: sortDirection,
|
||||||
this.pageSizeChange.emit(this.pageSize);
|
sortField: sortField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// (+) converts string to a number
|
||||||
|
this.currentPage = +page;
|
||||||
|
this.pageSize = +pageSize;
|
||||||
|
this.sortDirection = +sortDirection;
|
||||||
|
this.sortField = sortField;
|
||||||
|
this.pageChange.emit(this.currentPage);
|
||||||
|
this.pageSizeChange.emit(this.pageSize);
|
||||||
|
this.sortDirectionChange.emit(this.sortDirection);
|
||||||
|
this.sortFieldChange.emit(this.sortField);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure options passed contains the required properties.
|
* Ensure options passed contains the required properties.
|
||||||
*
|
*
|
||||||
* @param paginateOptions
|
* @param paginateOptions
|
||||||
* The paginate options object.
|
* The paginate options object.
|
||||||
*/
|
*/
|
||||||
private checkConfig(paginateOptions: any) {
|
private checkConfig(paginateOptions: any) {
|
||||||
let required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
|
let required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
|
||||||
let missing = required.filter(function (prop) { return !(prop in paginateOptions); });
|
let missing = required.filter(function (prop) {
|
||||||
if (0 < missing.length) {
|
return !(prop in paginateOptions);
|
||||||
throw new Error("Paginate: Argument is missing the following required properties: " + missing.join(', '));
|
});
|
||||||
|
if (0 < missing.length) {
|
||||||
|
throw new Error("Paginate: Argument is missing the following required properties: " + missing.join(', '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasMultiplePages(): boolean {
|
||||||
|
return this.collectionSize > this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldShowBottomPager(): boolean {
|
||||||
|
return this.hasMultiplePages || !this.hidePagerWhenSinglePage
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,6 +17,13 @@ import { NativeWindowFactory, NativeWindowService } from "./window.service";
|
|||||||
import { ComcolPageContentComponent } from "./comcol-page-content/comcol-page-content.component";
|
import { ComcolPageContentComponent } from "./comcol-page-content/comcol-page-content.component";
|
||||||
import { ComcolPageHeaderComponent } from "./comcol-page-header/comcol-page-header.component";
|
import { ComcolPageHeaderComponent } from "./comcol-page-header/comcol-page-header.component";
|
||||||
import { ComcolPageLogoComponent } from "./comcol-page-logo/comcol-page-logo.component";
|
import { ComcolPageLogoComponent } from "./comcol-page-logo/comcol-page-logo.component";
|
||||||
|
import { EnumKeysPipe } from "./utils/enum-keys-pipe";
|
||||||
|
import { ObjectListComponent } from "./object-list/object-list.component";
|
||||||
|
import { ObjectListElementComponent } from "../object-list/object-list-element/object-list-element.component";
|
||||||
|
import { ItemListElementComponent } from "../object-list/item-list-element/item-list-element.component";
|
||||||
|
import { CommunityListElementComponent } from "../object-list/community-list-element/community-list-element.component";
|
||||||
|
import { CollectionListElementComponent } from "../object-list/collection-list-element/collection-list-element.component";
|
||||||
|
import { TruncatePipe } from "./utils/truncate.pipe";
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -31,7 +38,9 @@ const MODULES = [
|
|||||||
|
|
||||||
const PIPES = [
|
const PIPES = [
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
SafeUrlPipe
|
SafeUrlPipe,
|
||||||
|
EnumKeysPipe,
|
||||||
|
TruncatePipe
|
||||||
// put pipes here
|
// put pipes here
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -41,7 +50,12 @@ const COMPONENTS = [
|
|||||||
ThumbnailComponent,
|
ThumbnailComponent,
|
||||||
ComcolPageContentComponent,
|
ComcolPageContentComponent,
|
||||||
ComcolPageHeaderComponent,
|
ComcolPageHeaderComponent,
|
||||||
ComcolPageLogoComponent
|
ComcolPageLogoComponent,
|
||||||
|
ObjectListComponent,
|
||||||
|
ObjectListElementComponent,
|
||||||
|
ItemListElementComponent,
|
||||||
|
CollectionListElementComponent,
|
||||||
|
CommunityListElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
13
src/app/shared/utils/enum-keys-pipe.ts
Normal file
13
src/app/shared/utils/enum-keys-pipe.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Pipe, PipeTransform } from "@angular/core";
|
||||||
|
@Pipe({ name: 'dsKeys' })
|
||||||
|
export class EnumKeysPipe implements PipeTransform {
|
||||||
|
transform(value, args: string[]): any {
|
||||||
|
let keys = [];
|
||||||
|
for (var enumMember in value) {
|
||||||
|
if (!isNaN(parseInt(enumMember, 10))) {
|
||||||
|
keys.push({ key: +enumMember, value: value[enumMember] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
26
src/app/shared/utils/truncate.pipe.ts
Normal file
26
src/app/shared/utils/truncate.pipe.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
import { hasValue } from "../empty.util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe to truncate a value in Angular. (Take a substring, starting at 0)
|
||||||
|
* Default value: 10
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'dsTruncate'
|
||||||
|
})
|
||||||
|
export class TruncatePipe implements PipeTransform {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
transform(value: string, args: Array<string>) : string {
|
||||||
|
if (hasValue(value)) {
|
||||||
|
let limit = (args && args.length > 0) ? parseInt(args[0], 10) : 10; // 10 as default truncate value
|
||||||
|
return value.length > limit ? value.substring(0, limit) + "..." : value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img *ngIf="thumbnail" [src]="thumbnail.retrieve"/>
|
<img *ngIf="thumbnail" [src]="thumbnail.retrieve" (error)="errorHandler($event)"/>
|
||||||
<img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/>
|
<img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Bitstream } from "../core/shared/bitstream.model";
|
import { Bitstream } from "../core/shared/bitstream.model";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,21 +14,24 @@ import { Bitstream } from "../core/shared/bitstream.model";
|
|||||||
})
|
})
|
||||||
export class ThumbnailComponent {
|
export class ThumbnailComponent {
|
||||||
|
|
||||||
@Input() thumbnail: Bitstream;
|
@Input() thumbnail: Bitstream;
|
||||||
|
|
||||||
data: any = {};
|
data: any = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default 'holder.js' image
|
* The default 'holder.js' image
|
||||||
*/
|
*/
|
||||||
holderSource: string = "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E";
|
holderSource: string = "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.universalInit();
|
this.universalInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
universalInit() {
|
universalInit() {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
errorHandler(event) {
|
||||||
|
event.currentTarget.src = this.holderSource;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user