Merge remote-tracking branch 'upstream/main' into w2p-74199_Admin-search-dialogs

This commit is contained in:
Yana De Pauw
2020-11-20 10:44:24 +01:00
53 changed files with 636 additions and 185 deletions

View File

@@ -17,10 +17,9 @@ coverage:
# Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY. # Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY.
patch: patch:
default: default:
# For each PR, make sure the coverage of the new code is within 1% of current overall coverage. # Enable informational mode, which just provides info to reviewers & always passes
# We let 'patch' be more lenient as we only require *project* coverage to not drop significantly. # https://docs.codecov.io/docs/commit-status#section-informational
target: auto informational: true
threshold: 1%
# Turn PR comments "off". This feature adds the code coverage summary as a # Turn PR comments "off". This feature adds the code coverage summary as a
# comment on each PR. See https://docs.codecov.io/docs/pull-request-comments # comment on each PR. See https://docs.codecov.io/docs/pull-request-comments

View File

@@ -12,3 +12,6 @@ trim_trailing_whitespace = true
[*.md] [*.md]
insert_final_newline = false insert_final_newline = false
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.ts]
quote_type = single

View File

@@ -88,6 +88,7 @@
"debug-loader": "^0.0.1", "debug-loader": "^0.0.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"express": "4.16.2", "express": "4.16.2",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^2.0.7", "fast-json-patch": "^2.0.7",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"filesize": "^6.1.0", "filesize": "^6.1.0",

View File

@@ -54,13 +54,6 @@ import(environmentFilePath)
function generateEnvironmentFile(file: GlobalConfig): void { function generateEnvironmentFile(file: GlobalConfig): void {
file.production = production; file.production = production;
buildBaseUrls(file); buildBaseUrls(file);
// TODO remove workaround in beta 5
if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
file.rest.nameSpace = getNameSpace(file.rest.nameSpace);
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${file.rest.nameSpace}'`));
}
const contents = `export const environment = ` + JSON.stringify(file); const contents = `export const environment = ` + JSON.stringify(file);
writeFile(targetPath, contents, (err) => { writeFile(targetPath, contents, (err) => {
if (err) { if (err) {
@@ -119,16 +112,5 @@ function getPort(port: number): string {
} }
function getNameSpace(nameSpace: string): string { function getNameSpace(nameSpace: string): string {
// TODO remove workaround in beta 5 return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
const apiMatches = nameSpace.match("(.*)/api/?$");
if (apiMatches != null) {
let newValue = '/'
if (hasValue(apiMatches[1])) {
newValue = apiMatches[1];
}
return newValue;
}
else {
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
}
} }

View File

@@ -28,12 +28,13 @@ import * as compression from 'compression';
import * as cookieParser from 'cookie-parser'; import * as cookieParser from 'cookie-parser';
import { join } from 'path'; import { join } from 'path';
import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment'; import { environment } from './src/environments/environment';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { hasValue, hasNoValue } from './src/app/shared/empty.util'; import { hasNoValue, hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface';
/* /*
* Set path for the browser application's dist folder * Set path for the browser application's dist folder
@@ -121,6 +122,19 @@ function cacheControl(req, res, next) {
next(); next();
} }
/**
* Checks if the rateLimiter property is present
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
*/
if (hasValue((environment.ui as UIServerConfig).rateLimiter)) {
const RateLimit = require('express-rate-limit');
const limiter = new RateLimit({
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
max: (environment.ui as UIServerConfig).rateLimiter.max
});
app.use(limiter);
}
/* /*
* Serve static resources (images, i18n messages, …) * Serve static resources (images, i18n messages, …)
*/ */
@@ -209,8 +223,9 @@ if (environment.ui.ssl) {
certificate: certificate certificate: certificate
}); });
} else { } else {
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
pem.createCertificate({ pem.createCertificate({
days: 1, days: 1,

View File

@@ -13,6 +13,7 @@ import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths'; import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
describe('CollectionAdminSearchResultGridElementComponent', () => { describe('CollectionAdminSearchResultGridElementComponent', () => {
let component: CollectionAdminSearchResultGridElementComponent; let component: CollectionAdminSearchResultGridElementComponent;
@@ -26,6 +27,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
searchResult.indexableObject = new Collection(); searchResult.indexableObject = new Collection();
searchResult.indexableObject.uuid = id; searchResult.indexableObject.uuid = id;
} }
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: {}
});
beforeEach(async(() => { beforeEach(async(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -39,6 +45,7 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} }, { provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService}
] ]
}) })
.compileComponents(); .compileComponents();

View File

@@ -14,8 +14,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component'; import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model'; import { Community } from '../../../../../core/shared/community.model';
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths'; import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
describe('CommunityAdminSearchResultGridElementComponent', () => { describe('CommunityAdminSearchResultGridElementComponent', () => {
let component: CommunityAdminSearchResultGridElementComponent; let component: CommunityAdminSearchResultGridElementComponent;
@@ -29,6 +29,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
searchResult.indexableObject = new Community(); searchResult.indexableObject = new Community();
searchResult.indexableObject.uuid = id; searchResult.indexableObject.uuid = id;
} }
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: {}
});
beforeEach(async(() => { beforeEach(async(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -42,6 +47,7 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} }, { provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCollectionPageComponent } from './create-collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import {RequestService} from '../../core/data/request.service';
describe('CreateCollectionPageComponent', () => { describe('CreateCollectionPageComponent', () => {
let comp: CreateCollectionPageComponent; let comp: CreateCollectionPageComponent;
@@ -29,7 +30,8 @@ describe('CreateCollectionPageComponent', () => {
}, },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} }, { provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() } { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: RequestService, useValue: {}}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -7,6 +7,7 @@ import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {RequestService} from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can create a new Collection * Component that represents the page where a user can create a new Collection
@@ -26,8 +27,9 @@ export class CreateCollectionPageComponent extends CreateComColPageComponent<Col
protected routeService: RouteService, protected routeService: RouteService,
protected router: Router, protected router: Router,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate); super(collectionDataService, communityDataService, routeService, router, notificationsService, translate, requestService);
} }
} }

View File

@@ -9,6 +9,7 @@ import { of as observableOf } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { DeleteCollectionPageComponent } from './delete-collection-page.component'; import { DeleteCollectionPageComponent } from './delete-collection-page.component';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import {RequestService} from '../../core/data/request.service';
describe('DeleteCollectionPageComponent', () => { describe('DeleteCollectionPageComponent', () => {
let comp: DeleteCollectionPageComponent; let comp: DeleteCollectionPageComponent;
@@ -22,6 +23,7 @@ describe('DeleteCollectionPageComponent', () => {
{ provide: CollectionDataService, useValue: {} }, { provide: CollectionDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
{ provide: NotificationsService, useValue: {} }, { provide: NotificationsService, useValue: {} },
{ provide: RequestService, useValue: {} }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -5,6 +5,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {RequestService} from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can delete an existing Collection * Component that represents the page where a user can delete an existing Collection
@@ -22,8 +23,9 @@ export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Col
protected router: Router, protected router: Router,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notifications: NotificationsService, protected notifications: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(dsoDataService, router, route, notifications, translate); super(dsoDataService, router, route, notifications, translate, requestService);
} }
} }

View File

@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCommunityPageComponent } from './create-community-page.component'; import { CreateCommunityPageComponent } from './create-community-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import {RequestService} from '../../core/data/request.service';
describe('CreateCommunityPageComponent', () => { describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent; let comp: CreateCommunityPageComponent;
@@ -25,7 +26,8 @@ describe('CreateCommunityPageComponent', () => {
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} }, { provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() } { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: RequestService, useValue: {} }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -6,6 +6,7 @@ import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {RequestService} from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can create a new Community * Component that represents the page where a user can create a new Community
@@ -24,8 +25,9 @@ export class CreateCommunityPageComponent extends CreateComColPageComponent<Comm
protected routeService: RouteService, protected routeService: RouteService,
protected router: Router, protected router: Router,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(communityDataService, communityDataService, routeService, router, notificationsService, translate); super(communityDataService, communityDataService, routeService, router, notificationsService, translate, requestService);
} }
} }

View File

@@ -9,6 +9,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { DeleteCommunityPageComponent } from './delete-community-page.component'; import { DeleteCommunityPageComponent } from './delete-community-page.component';
import {RequestService} from '../../core/data/request.service';
describe('DeleteCommunityPageComponent', () => { describe('DeleteCommunityPageComponent', () => {
let comp: DeleteCommunityPageComponent; let comp: DeleteCommunityPageComponent;
@@ -22,6 +23,7 @@ describe('DeleteCommunityPageComponent', () => {
{ provide: CommunityDataService, useValue: {} }, { provide: CommunityDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
{ provide: NotificationsService, useValue: {} }, { provide: NotificationsService, useValue: {} },
{ provide: RequestService, useValue: {}}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {RequestService} from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can delete an existing Community * Component that represents the page where a user can delete an existing Community
@@ -22,8 +23,10 @@ export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Comm
protected router: Router, protected router: Router,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notifications: NotificationsService, protected notifications: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(dsoDataService, router, route, notifications, translate); super(dsoDataService, router, route, notifications, translate, requestService);
} }
} }

View File

@@ -13,7 +13,6 @@ import { RouterTestingModule } from '@angular/router/testing';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';

View File

@@ -8,7 +8,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { MetadataService } from '../../core/metadata/metadata.service'; import { MetadataService } from '../../core/metadata/metadata.service';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';

View File

@@ -1,9 +1,10 @@
import { NgZone } from '@angular/core'; import { Subscription } from 'rxjs/internal/Subscription';
import { FindListOptions } from '../core/data/request.models'; import { FindListOptions } from '../core/data/request.models';
import { hasValue } from '../shared/empty.util';
import { CommunityListService, FlatNode } from './community-list-service'; import { CommunityListService, FlatNode } from './community-list-service';
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { BehaviorSubject, Observable, } from 'rxjs'; import { BehaviorSubject, Observable, } from 'rxjs';
import { finalize, take, } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
/** /**
* DataSource object needed by a CDK Tree to render its nodes. * DataSource object needed by a CDK Tree to render its nodes.
@@ -15,9 +16,9 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
private communityList$ = new BehaviorSubject<FlatNode[]>([]); private communityList$ = new BehaviorSubject<FlatNode[]>([]);
public loading$ = new BehaviorSubject<boolean>(false); public loading$ = new BehaviorSubject<boolean>(false);
private subLoadCommunities: Subscription;
constructor(private communityListService: CommunityListService, constructor(private communityListService: CommunityListService) {
private zone: NgZone) {
} }
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> { connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
@@ -26,13 +27,13 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) { loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
this.loading$.next(true); this.loading$.next(true);
this.zone.runOutsideAngular(() => { if (hasValue(this.subLoadCommunities)) {
this.communityListService.loadCommunities(findOptions, expandedNodes).pipe( this.subLoadCommunities.unsubscribe();
take(1), }
finalize(() => this.zone.run(() => this.loading$.next(false))), this.subLoadCommunities = this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
).subscribe((flatNodes: FlatNode[]) => { finalize(() => this.loading$.next(false)),
this.zone.run(() => this.communityList$.next(flatNodes)); ).subscribe((flatNodes: FlatNode[]) => {
}); this.communityList$.next(flatNodes);
}); });
} }

View File

@@ -2,13 +2,12 @@ import { Injectable } from '@angular/core';
import { createSelector, Store } from '@ngrx/store'; import { createSelector, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { FindListOptions } from '../core/data/request.models'; import { FindListOptions } from '../core/data/request.models';
import { map, flatMap } from 'rxjs/operators';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { getSucceededRemoteData } from '../core/shared/operators';
import { PageInfo } from '../core/shared/page-info.model'; import { PageInfo } from '../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../shared/empty.util'; import { hasValue, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
@@ -148,7 +147,7 @@ export class CommunityListService {
return new PaginatedList(newPageInfo, newPage); return new PaginatedList(newPageInfo, newPage);
}) })
); );
return topComs$.pipe(flatMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes))); return topComs$.pipe(switchMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
}; };
/** /**
@@ -228,9 +227,13 @@ export class CommunityListService {
currentPage: i currentPage: i
}) })
.pipe( .pipe(
getSucceededRemoteData(), switchMap((rd: RemoteData<PaginatedList<Community>>) => {
flatMap((rd: RemoteData<PaginatedList<Community>>) => if (hasValue(rd) && hasValue(rd.payload)) {
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes)) return this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes);
} else {
return [];
}
})
); );
subcoms = [...subcoms, nextSetOfSubcommunitiesPage]; subcoms = [...subcoms, nextSetOfSubcommunitiesPage];
@@ -246,14 +249,17 @@ export class CommunityListService {
currentPage: i currentPage: i
}) })
.pipe( .pipe(
getSucceededRemoteData(),
map((rd: RemoteData<PaginatedList<Collection>>) => { map((rd: RemoteData<PaginatedList<Collection>>) => {
let nodes = rd.payload.page if (hasValue(rd) && hasValue(rd.payload)) {
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); let nodes = rd.payload.page
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) { .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
}
return nodes;
} else {
return [];
} }
return nodes;
}), }),
); );
collections = [...collections, nextSetOfCollectionsPage]; collections = [...collections, nextSetOfCollectionsPage];
@@ -275,14 +281,24 @@ export class CommunityListService {
let hasColls$: Observable<boolean>; let hasColls$: Observable<boolean>;
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 }) hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
.pipe( .pipe(
getSucceededRemoteData(), map((rd: RemoteData<PaginatedList<Community>>) => {
map((results) => results.payload.totalElements > 0), if (hasValue(rd) && hasValue(rd.payload)) {
return rd.payload.totalElements > 0;
} else {
return false;
}
}),
); );
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
.pipe( .pipe(
getSucceededRemoteData(), map((rd: RemoteData<PaginatedList<Collection>>) => {
map((results) => results.payload.totalElements > 0), if (hasValue(rd) && hasValue(rd.payload)) {
return rd.payload.totalElements > 0;
} else {
return false;
}
}),
); );
let hasChildren$: Observable<boolean>; let hasChildren$: Observable<boolean>;

View File

@@ -28,7 +28,7 @@
<button type="button" class="btn btn-default" cdkTreeNodeToggle <button type="button" class="btn btn-default" cdkTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name" [attr.aria-label]="'toggle ' + node.name"
(click)="toggleExpanded(node)" (click)="toggleExpanded(node)"
[ngClass]="(node.isExpandable$ | async) ? 'visible' : 'invisible'"> [ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}" <span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span> aria-hidden="true"></span>
</button> </button>

View File

@@ -1,4 +1,4 @@
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../core/data/request.models'; import { FindListOptions } from '../../core/data/request.models';
@@ -24,15 +24,14 @@ export class CommunityListComponent implements OnInit, OnDestroy {
public loadingNode: FlatNode; public loadingNode: FlatNode;
treeControl = new FlatTreeControl<FlatNode>( treeControl = new FlatTreeControl<FlatNode>(
(node) => node.level, (node) => true (node: FlatNode) => node.level, (node: FlatNode) => true
); );
dataSource: CommunityListDatasource; dataSource: CommunityListDatasource;
paginationConfig: FindListOptions; paginationConfig: FindListOptions;
constructor(private communityListService: CommunityListService, constructor(private communityListService: CommunityListService) {
private zone: NgZone) {
this.paginationConfig = new FindListOptions(); this.paginationConfig = new FindListOptions();
this.paginationConfig.elementsPerPage = 2; this.paginationConfig.elementsPerPage = 2;
this.paginationConfig.currentPage = 1; this.paginationConfig.currentPage = 1;
@@ -40,7 +39,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.dataSource = new CommunityListDatasource(this.communityListService, this.zone); this.dataSource = new CommunityListDatasource(this.communityListService);
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => { this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
this.loadingNode = result; this.loadingNode = result;
}); });
@@ -65,7 +64,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
} }
/** /**
* Toggles the expanded variable of a node, adds it to the exapanded nodes list and reloads the tree so this node is expanded * Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded
* @param node Node we want to expand * @param node Node we want to expand
*/ */
toggleExpanded(node: FlatNode) { toggleExpanded(node: FlatNode) {

View File

@@ -6,7 +6,6 @@ import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model'; import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import * as decorators from './build-decorators'; import * as decorators from './build-decorators';
import { getDataServiceFor } from './build-decorators';
import { LinkService } from './link.service'; import { LinkService } from './link.service';
const spyOnFunction = <T>(obj: T, func: keyof T) => { const spyOnFunction = <T>(obj: T, func: keyof T) => {

View File

@@ -27,7 +27,7 @@ export class LinkService {
*/ */
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>): T { public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>): T {
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => { linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
this.resolveLink(model, linkToFollow); this.resolveLink(model, linkToFollow);
}); });
return model; return model;
} }

View File

@@ -17,6 +17,7 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindByIDRequest, FindListOptions } from './request.models'; import { FindByIDRequest, FindListOptions } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import {createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../../shared/remote-data.utils';
const LINK_NAME = 'test'; const LINK_NAME = 'test';
@@ -51,7 +52,9 @@ describe('ComColDataService', () => {
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
let halService: any = {}; let halService: any = {};
const rdbService = {} as RemoteDataBuildService; const rdbService = {
buildSingle : () => null
} as any;
const store = {} as Store<CoreState>; const store = {} as Store<CoreState>;
const notificationsService = {} as NotificationsService; const notificationsService = {} as NotificationsService;
const http = {} as HttpClient; const http = {} as HttpClient;
@@ -178,6 +181,90 @@ describe('ComColDataService', () => {
}); });
}); });
describe('cache refresh', () => {
let communityWithoutParentHref;
let data;
beforeEach(() => {
scheduler = getTestScheduler();
halService = {
getEndpoint: (linkPath) => 'https://rest.api/core/' + linkPath
};
service = initTestService();
})
describe('cache refreshed top level community', () => {
beforeEach(() => {
spyOn(rdbService, 'buildSingle').and.returnValue(createNoContentRemoteDataObject$());
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'top level community'
}]
}),
_links: {
parentCommunity: {
href: 'topLevel/parentCommunity'
}
}
};
communityWithoutParentHref = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'top level community'
}]
}),
_links: {}
};
});
it('top level community cache refreshed', () => {
scheduler.schedule(() => (service as any).refreshCache(data));
scheduler.flush();
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith('https://rest.api/core/communities/search/top');
});
it('top level community without parent link, cache not refreshed', () => {
scheduler.schedule(() => (service as any).refreshCache(communityWithoutParentHref));
scheduler.flush();
expect(requestService.removeByHrefSubstring).not.toHaveBeenCalled();
});
});
describe('cache refreshed child community', () => {
beforeEach(() => {
const parentCommunity = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66as300d399',
id: 'a20da287-e174-466a-9926-f66as300d399',
metadata: [{
key: 'dc.title',
value: 'parent community'
}],
_links: {}
});
spyOn(rdbService, 'buildSingle').and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity));
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'child community'
}]
}),
_links: {
parentCommunity: {
href: 'child/parentCommunity'
}
}
};
});
it('child level community cache refreshed', () => {
scheduler.schedule(() => (service as any).refreshCache(data));
scheduler.flush();
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399');
});
});
});
}); });
}); });

View File

@@ -21,12 +21,14 @@ import {
configureRequest, configureRequest,
getRemoteDataPayload, getRemoteDataPayload,
getResponseFromEntry, getResponseFromEntry,
getSucceededOrNoContentResponse,
getSucceededRemoteData getSucceededRemoteData
} from '../shared/operators'; } from '../shared/operators';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import {Collection} from '../shared/collection.model';
export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> { export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> {
protected abstract cds: CommunityDataService; protected abstract cds: CommunityDataService;
@@ -119,4 +121,23 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
); );
} }
} }
public refreshCache(dso: T) {
const parentCommunityUrl = this.parentCommunityUrlLookup(dso as any);
if (!hasValue(parentCommunityUrl)) {
return;
}
this.findByHref(parentCommunityUrl).pipe(
getSucceededOrNoContentResponse(),
take(1),
).subscribe((rd: RemoteData<any>) => {
const href = rd.hasSucceeded && !isEmpty(rd.payload.id) ? rd.payload.id : this.halService.getEndpoint('communities/search/top');
this.requestService.removeByHrefSubstring(href)
});
}
private parentCommunityUrlLookup(dso: Collection | Community) {
const parentCommunity = dso._links.parentCommunity;
return isNotEmpty(parentCommunity) ? parentCommunity.href : null;
}
} }

View File

@@ -55,4 +55,8 @@ export class RemoteData<T> {
return this.state === RemoteDataState.Success; return this.state === RemoteDataState.Success;
} }
get hasNoContent(): boolean {
return this.statusCode === 204;
}
} }

View File

@@ -75,6 +75,10 @@ export const getSucceededRemoteWithNotEmptyData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> => <T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded && isNotEmpty(rd.payload))); source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded && isNotEmpty(rd.payload)));
export const getSucceededOrNoContentResponse = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded || rd.hasNoContent));
/** /**
* Get the first successful remotely retrieved object * Get the first successful remotely retrieved object
* *

View File

@@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../../core/services/route.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import {TranslateModule, TranslateService} from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { SharedModule } from '../../shared.module'; import { SharedModule } from '../../shared.module';
@@ -12,12 +12,14 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { CreateComColPageComponent } from './create-comcol-page.component'; import { CreateComColPageComponent } from './create-comcol-page.component';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$, createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from '../../remote-data.utils'; } from '../../remote-data.utils';
import { ComColDataService } from '../../../core/data/comcol-data.service'; import { ComColDataService } from '../../../core/data/comcol-data.service';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { RequestService } from '../../../core/data/request.service';
import {getTestScheduler} from 'jasmine-marbles';
describe('CreateComColPageComponent', () => { describe('CreateComColPageComponent', () => {
let comp: CreateComColPageComponent<DSpaceObject>; let comp: CreateComColPageComponent<DSpaceObject>;
@@ -29,9 +31,12 @@ describe('CreateComColPageComponent', () => {
let community; let community;
let newCommunity; let newCommunity;
let parentCommunity;
let communityDataServiceStub; let communityDataServiceStub;
let routeServiceStub; let routeServiceStub;
let routerStub; let routerStub;
let requestServiceStub;
let scheduler;
const logoEndpoint = 'rest/api/logo/endpoint'; const logoEndpoint = 'rest/api/logo/endpoint';
@@ -41,7 +46,18 @@ describe('CreateComColPageComponent', () => {
metadata: [{ metadata: [{
key: 'dc.title', key: 'dc.title',
value: 'test community' value: 'test community'
}] }],
_links: {}
});
parentCommunity = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66as300d399',
id: 'a20da287-e174-466a-9926-f66as300d399',
metadata: [{
key: 'dc.title',
value: 'parent community'
}],
_links: {}
}); });
newCommunity = Object.assign(new Community(), { newCommunity = Object.assign(new Community(), {
@@ -49,7 +65,8 @@ describe('CreateComColPageComponent', () => {
metadata: [{ metadata: [{
key: 'dc.title', key: 'dc.title',
value: 'new community' value: 'new community'
}] }],
_links: {}
}); });
communityDataServiceStub = { communityDataServiceStub = {
@@ -61,7 +78,9 @@ describe('CreateComColPageComponent', () => {
}] }]
})), })),
create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
getLogoEndpoint: () => observableOf(logoEndpoint) getLogoEndpoint: () => observableOf(logoEndpoint),
findByHref: () => null,
refreshCache: () => {return}
}; };
routeServiceStub = { routeServiceStub = {
@@ -71,6 +90,10 @@ describe('CreateComColPageComponent', () => {
navigate: (commands) => commands navigate: (commands) => commands
}; };
requestServiceStub = jasmine.createSpyObj('RequestService', {
removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring'),
});
} }
beforeEach(async(() => { beforeEach(async(() => {
@@ -82,7 +105,8 @@ describe('CreateComColPageComponent', () => {
{ provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() } { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: RequestService, useValue: requestServiceStub}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -97,6 +121,7 @@ describe('CreateComColPageComponent', () => {
communityDataService = (comp as any).communityDataService; communityDataService = (comp as any).communityDataService;
routeService = (comp as any).routeService; routeService = (comp as any).routeService;
router = (comp as any).router; router = (comp as any).router;
scheduler = getTestScheduler();
}); });
describe('onSubmit', () => { describe('onSubmit', () => {
@@ -111,6 +136,7 @@ describe('CreateComColPageComponent', () => {
value: 'test' value: 'test'
}] }]
}), }),
_links: {},
uploader: { uploader: {
options: { options: {
url: '' url: ''
@@ -123,19 +149,23 @@ describe('CreateComColPageComponent', () => {
}; };
}); });
it('should navigate when successful', () => { it('should navigate and refresh cache when successful', () => {
spyOn(router, 'navigate'); spyOn(router, 'navigate');
comp.onSubmit(data); spyOn((dsoDataService as any), 'refreshCache')
fixture.detectChanges(); scheduler.schedule(() => comp.onSubmit(data));
scheduler.flush();
expect(router.navigate).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled();
expect((dsoDataService as any).refreshCache).toHaveBeenCalled();
}); });
it('should not navigate on failure', () => { it('should neither navigate nor refresh cache on failure', () => {
spyOn(router, 'navigate'); spyOn(router, 'navigate');
spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity)); spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity));
comp.onSubmit(data); spyOn(dsoDataService, 'refreshCache')
fixture.detectChanges(); scheduler.schedule(() => comp.onSubmit(data));
scheduler.flush();
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
expect((dsoDataService as any).refreshCache).not.toHaveBeenCalled();
}); });
}); });
@@ -148,6 +178,7 @@ describe('CreateComColPageComponent', () => {
value: 'test' value: 'test'
}] }]
}), }),
_links: {},
uploader: { uploader: {
options: { options: {
url: '' url: ''
@@ -164,21 +195,21 @@ describe('CreateComColPageComponent', () => {
it('should not navigate', () => { it('should not navigate', () => {
spyOn(router, 'navigate'); spyOn(router, 'navigate');
comp.onSubmit(data); scheduler.schedule(() => comp.onSubmit(data));
fixture.detectChanges(); scheduler.flush();
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
}); });
it('should set the uploader\'s url to the logo\'s endpoint', () => { it('should set the uploader\'s url to the logo\'s endpoint', () => {
comp.onSubmit(data); scheduler.schedule(() => comp.onSubmit(data));
fixture.detectChanges(); scheduler.flush();
expect(data.uploader.options.url).toEqual(logoEndpoint); expect(data.uploader.options.url).toEqual(logoEndpoint);
}); });
it('should call the uploader\'s uploadAll', () => { it('should call the uploader\'s uploadAll', () => {
spyOn(data.uploader, 'uploadAll'); spyOn(data.uploader, 'uploadAll');
comp.onSubmit(data); scheduler.schedule(() => comp.onSubmit(data));
fixture.detectChanges(); scheduler.flush();
expect(data.uploader.uploadAll).toHaveBeenCalled(); expect(data.uploader.uploadAll).toHaveBeenCalled();
}); });
}); });

View File

@@ -2,18 +2,21 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import {flatMap, take} from 'rxjs/operators';
import { ComColDataService } from '../../../core/data/comcol-data.service'; import { ComColDataService } from '../../../core/data/comcol-data.service';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../../core/services/route.service';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { getSucceededRemoteData } from '../../../core/shared/operators'; import {
getFirstSucceededRemoteDataPayload,
} from '../../../core/shared/operators';
import { ResourceType } from '../../../core/shared/resource-type'; import { ResourceType } from '../../../core/shared/resource-type';
import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; import {hasValue, isNotEmpty, isNotUndefined} from '../../empty.util';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { RequestParam } from '../../../core/cache/models/request-param.model'; import { RequestParam } from '../../../core/cache/models/request-param.model';
import {RequestService} from '../../../core/data/request.service';
/** /**
* Component representing the create page for communities and collections * Component representing the create page for communities and collections
@@ -54,7 +57,8 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
protected routeService: RouteService, protected routeService: RouteService,
protected router: Router, protected router: Router,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
} }
@@ -76,25 +80,29 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
const dso = event.dso; const dso = event.dso;
const uploader = event.uploader; const uploader = event.uploader;
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.parentUUID$.pipe(
take(1),
flatMap((uuid: string) => {
const params = uuid ? [new RequestParam('parent', uuid)] : []; const params = uuid ? [new RequestParam('parent', uuid)] : [];
this.dsoDataService.create(dso, ...params) return this.dsoDataService.create(dso, ...params)
.pipe(getSucceededRemoteData()) .pipe(getFirstSucceededRemoteDataPayload()
.subscribe((dsoRD: RemoteData<TDomain>) => { )
if (isNotUndefined(dsoRD)) { }))
this.newUUID = dsoRD.payload.uuid; .subscribe((dsoRD: TDomain) => {
if (uploader.queue.length > 0) { if (isNotUndefined(dsoRD)) {
this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => { this.newUUID = dsoRD.uuid;
uploader.options.url = href; if (uploader.queue.length > 0) {
uploader.uploadAll(); this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => {
}); uploader.options.url = href;
} else { uploader.uploadAll();
this.navigateToNewPage(); });
} } else {
this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); this.navigateToNewPage();
} }
}); this.dsoDataService.refreshCache(dsoRD);
}); }
this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success'));
});
} }
/** /**
@@ -105,5 +113,4 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
this.router.navigate([this.frontendURL + this.newUUID]); this.router.navigate([this.frontendURL + this.newUUID]);
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import {TranslateModule, TranslateService} from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { SharedModule } from '../../shared.module'; import { SharedModule } from '../../shared.module';
@@ -13,6 +13,10 @@ import { DataService } from '../../../core/data/data.service';
import { DeleteComColPageComponent } from './delete-comcol-page.component'; import { DeleteComColPageComponent } from './delete-comcol-page.component';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import {RequestService} from '../../../core/data/request.service';
import {getTestScheduler} from 'jasmine-marbles';
import {createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../../remote-data.utils';
import {ComColDataService} from '../../../core/data/comcol-data.service';
describe('DeleteComColPageComponent', () => { describe('DeleteComColPageComponent', () => {
let comp: DeleteComColPageComponent<DSpaceObject>; let comp: DeleteComColPageComponent<DSpaceObject>;
@@ -22,9 +26,15 @@ describe('DeleteComColPageComponent', () => {
let community; let community;
let newCommunity; let newCommunity;
let parentCommunity;
let routerStub; let routerStub;
let routeStub; let routeStub;
let notificationsService; let notificationsService;
let translateServiceStub;
let requestServiceStub;
let scheduler;
const validUUID = 'valid-uuid'; const validUUID = 'valid-uuid';
const invalidUUID = 'invalid-uuid'; const invalidUUID = 'invalid-uuid';
const frontendURL = '/testType'; const frontendURL = '/testType';
@@ -45,10 +55,21 @@ describe('DeleteComColPageComponent', () => {
}] }]
}); });
parentCommunity = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66as300d399',
id: 'a20da287-e174-466a-9926-f66as300d399',
metadata: [{
key: 'dc.title',
value: 'parent community'
}]
});
dsoDataService = jasmine.createSpyObj( dsoDataService = jasmine.createSpyObj(
'dsoDataService', 'dsoDataService',
{ {
delete: observableOf({ isSuccessful: true }) delete: observableOf({ isSuccessful: true }),
findByHref: jasmine.createSpy('findByHref'),
refreshCache: jasmine.createSpy('refreshCache')
}); });
routerStub = { routerStub = {
@@ -59,6 +80,14 @@ describe('DeleteComColPageComponent', () => {
data: observableOf(community) data: observableOf(community)
}; };
requestServiceStub = jasmine.createSpyObj('RequestService', {
removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring')
});
translateServiceStub = jasmine.createSpyObj('TranslateService', {
instant: jasmine.createSpy('instant')
});
} }
beforeEach(async(() => { beforeEach(async(() => {
@@ -66,10 +95,12 @@ describe('DeleteComColPageComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
providers: [ providers: [
{ provide: DataService, useValue: dsoDataService }, { provide: ComColDataService, useValue: dsoDataService },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub},
{ provide: RequestService, useValue: requestServiceStub}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -82,43 +113,63 @@ describe('DeleteComColPageComponent', () => {
notificationsService = (comp as any).notifications; notificationsService = (comp as any).notifications;
(comp as any).frontendURL = frontendURL; (comp as any).frontendURL = frontendURL;
router = (comp as any).router; router = (comp as any).router;
scheduler = getTestScheduler();
}); });
describe('onConfirm', () => { describe('onConfirm', () => {
let data1; let data1;
let data2; let data2;
beforeEach(() => { beforeEach(() => {
data1 = Object.assign(new Community(), { data1 = {
uuid: validUUID, dso: Object.assign(new Community(), {
metadata: [{ uuid: validUUID,
key: 'dc.title', metadata: [{
value: 'test' key: 'dc.title',
}] value: 'test'
}); }]
}),
_links: {}
};
data2 = Object.assign(new Community(), { data2 = {
uuid: invalidUUID, dso: Object.assign(new Community(), {
metadata: [{ uuid: invalidUUID,
key: 'dc.title', metadata: [{
value: 'test' key: 'dc.title',
}] value: 'test'
}); }]
}),
_links: {},
uploader: {
options: {
url: ''
},
queue: [],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
};
}); });
it('should show an error notification on failure', () => { it('should show an error notification on failure', () => {
(dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false })); (dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false }));
spyOn(router, 'navigate'); spyOn(router, 'navigate');
comp.onConfirm(data2); scheduler.schedule(() => comp.onConfirm(data2));
scheduler.flush();
fixture.detectChanges(); fixture.detectChanges();
expect(notificationsService.error).toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled();
expect(dsoDataService.refreshCache).not.toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled();
}); });
it('should show a success notification on success and navigate', () => { it('should show a success notification on success and navigate', () => {
spyOn(router, 'navigate'); spyOn(router, 'navigate');
comp.onConfirm(data1); scheduler.schedule(() => comp.onConfirm(data1));
scheduler.flush();
fixture.detectChanges(); fixture.detectChanges();
expect(notificationsService.success).toHaveBeenCalled(); expect(notificationsService.success).toHaveBeenCalled();
expect(dsoDataService.refreshCache).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled();
}); });

View File

@@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { Observable } from 'rxjs'; import {Observable} from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { RemoteData } from '../../../core/data/remote-data'; import {RemoteData} from '../../../core/data/remote-data';
import { first, map } from 'rxjs/operators'; import {first, map} from 'rxjs/operators';
import { DataService } from '../../../core/data/data.service'; import {DSpaceObject} from '../../../core/shared/dspace-object.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import {NotificationsService} from '../../notifications/notifications.service';
import { NotificationsService } from '../../notifications/notifications.service'; import {TranslateService} from '@ngx-translate/core';
import { TranslateService } from '@ngx-translate/core'; import {RestResponse} from '../../../core/cache/response.models';
import { RestResponse } from '../../../core/cache/response.models'; import {RequestService} from '../../../core/data/request.service';
import {ComColDataService} from '../../../core/data/comcol-data.service';
/** /**
* Component representing the delete page for communities and collections * Component representing the delete page for communities and collections
@@ -27,11 +28,12 @@ export class DeleteComColPageComponent<TDomain extends DSpaceObject> implements
public dsoRD$: Observable<RemoteData<TDomain>>; public dsoRD$: Observable<RemoteData<TDomain>>;
public constructor( public constructor(
protected dsoDataService: DataService<TDomain>, protected dsoDataService: ComColDataService<TDomain>,
protected router: Router, protected router: Router,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notifications: NotificationsService, protected notifications: NotificationsService,
protected translate: TranslateService protected translate: TranslateService,
protected requestService: RequestService
) { ) {
} }
@@ -50,6 +52,7 @@ export class DeleteComColPageComponent<TDomain extends DSpaceObject> implements
if (response.isSuccessful) { if (response.isSuccessful) {
const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success'); const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success');
this.notifications.success(successMessage) this.notifications.success(successMessage)
this.dsoDataService.refreshCache(dso);
} else { } else {
const errorMessage = this.translate.instant((dso as any).type + '.delete.notification.fail'); const errorMessage = this.translate.instant((dso as any).type + '.delete.notification.fail');
this.notifications.error(errorMessage) this.notifications.error(errorMessage)

View File

@@ -1,10 +1,10 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.logo"> <ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.logo"> <ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -3,6 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { LinkService } from '../../../core/cache/builders/link.service';
let collectionGridElementComponent: CollectionGridElementComponent; let collectionGridElementComponent: CollectionGridElementComponent;
let fixture: ComponentFixture<CollectionGridElementComponent>; let fixture: ComponentFixture<CollectionGridElementComponent>;
@@ -29,12 +30,17 @@ const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection()
} }
}); });
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: mockCollectionWithAbstract
});
describe('CollectionGridElementComponent', () => { describe('CollectionGridElementComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ CollectionGridElementComponent ], declarations: [ CollectionGridElementComponent ],
providers: [ providers: [
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)} { provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)},
{ provide: LinkService, useValue: linkService}
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]

View File

@@ -1,9 +1,11 @@
import { Component, Inject } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ViewMode } from '../../../core/shared/view-mode.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { hasNoValue, hasValue } from '../../empty.util';
import { followLink } from '../../utils/follow-link-config.model';
import { LinkService } from '../../../core/cache/builders/link.service';
/** /**
* Component representing a grid element for collection * Component representing a grid element for collection
@@ -11,8 +13,29 @@ import { listableObjectComponent } from '../../object-collection/shared/listable
@Component({ @Component({
selector: 'ds-collection-grid-element', selector: 'ds-collection-grid-element',
styleUrls: ['./collection-grid-element.component.scss'], styleUrls: ['./collection-grid-element.component.scss'],
templateUrl: './collection-grid-element.component.html' templateUrl: './collection-grid-element.component.html',
}) })
@listableObjectComponent(Collection, ViewMode.GridElement) @listableObjectComponent(Collection, ViewMode.GridElement)
export class CollectionGridElementComponent extends AbstractListableElementComponent<Collection> {} export class CollectionGridElementComponent extends AbstractListableElementComponent<
Collection
> {
private _object: Collection;
constructor(private linkService: LinkService) {
super();
}
@Input() set object(object: Collection) {
this._object = object;
if (hasValue(this._object) && hasNoValue(this._object.logo)) {
this.linkService.resolveLink<Collection>(
this._object,
followLink('logo')
);
}
}
get object(): Collection {
return this._object;
}
}

View File

@@ -1,10 +1,10 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.logo"> <ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.logo"> <ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -3,6 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { LinkService } from '../../../core/cache/builders/link.service';
let communityGridElementComponent: CommunityGridElementComponent; let communityGridElementComponent: CommunityGridElementComponent;
let fixture: ComponentFixture<CommunityGridElementComponent>; let fixture: ComponentFixture<CommunityGridElementComponent>;
@@ -29,12 +30,17 @@ const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
} }
}); });
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: mockCommunityWithAbstract
});
describe('CommunityGridElementComponent', () => { describe('CommunityGridElementComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ CommunityGridElementComponent ], declarations: [ CommunityGridElementComponent ],
providers: [ providers: [
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)} { provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)},
{ provide: LinkService, useValue: linkService}
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]

View File

@@ -1,9 +1,11 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ViewMode } from '../../../core/shared/view-mode.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { followLink } from '../../utils/follow-link-config.model';
import { LinkService } from '../../../core/cache/builders/link.service';
import { hasNoValue, hasValue } from '../../empty.util';
/** /**
* Component representing a grid element for a community * Component representing a grid element for a community
@@ -15,4 +17,21 @@ import { listableObjectComponent } from '../../object-collection/shared/listable
}) })
@listableObjectComponent(Community, ViewMode.GridElement) @listableObjectComponent(Community, ViewMode.GridElement)
export class CommunityGridElementComponent extends AbstractListableElementComponent<Community> {} export class CommunityGridElementComponent extends AbstractListableElementComponent<Community> {
private _object: Community;
constructor( private linkService: LinkService) {
super();
}
@Input() set object(object: Community) {
this._object = object;
if (hasValue(this._object) && hasNoValue(this._object.logo)) {
this.linkService.resolveLink<Community>(this._object, followLink('logo'))
}
}
get object(): Community {
return this._object;
}
}

View File

@@ -1,3 +1,3 @@
<div class="thumbnail"> <div class="thumbnail">
<img [src]="src | dsSafeUrl" (error)="errorHandler($event)"/> <img [src]="src | dsSafeUrl" (error)="errorHandler($event)" />
</div> </div>

View File

@@ -1,4 +1,10 @@
import { Component, Input, OnInit } from '@angular/core'; import {
Component,
Input,
OnChanges,
OnInit,
SimpleChanges,
} from '@angular/core';
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { hasValue } from '../../empty.util'; import { hasValue } from '../../empty.util';
@@ -11,10 +17,9 @@ import { hasValue } from '../../empty.util';
@Component({ @Component({
selector: 'ds-grid-thumbnail', selector: 'ds-grid-thumbnail',
styleUrls: ['./grid-thumbnail.component.scss'], styleUrls: ['./grid-thumbnail.component.scss'],
templateUrl: './grid-thumbnail.component.html' templateUrl: './grid-thumbnail.component.html',
}) })
export class GridThumbnailComponent implements OnInit { export class GridThumbnailComponent implements OnInit, OnChanges {
@Input() thumbnail: Bitstream; @Input() thumbnail: Bitstream;
data: any = {}; data: any = {};
@@ -22,19 +27,47 @@ export class GridThumbnailComponent implements OnInit {
/** /**
* The default 'holder.js' image * The default 'holder.js' image
*/ */
@Input() defaultImage? = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; @Input() defaultImage? =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+';
src: string; src: string;
errorHandler(event) { errorHandler(event) {
event.currentTarget.src = this.defaultImage; event.currentTarget.src = this.defaultImage;
} }
/**
* Initialize the src
*/
ngOnInit(): void { ngOnInit(): void {
if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && this.thumbnail._links.content.href) { this.src = this.defaultImage;
this.src = this.thumbnail._links.content.href;
} else { this.checkThumbnail(this.thumbnail);
this.src = this.defaultImage }
/**
* If the old input is undefined and the new one is a bitsream then set src
*/
ngOnChanges(changes: SimpleChanges): void {
if (
!hasValue(changes.thumbnail.previousValue) &&
hasValue(changes.thumbnail.currentValue)
) {
console.log('this.thumbnail', changes.thumbnail.currentValue);
this.checkThumbnail(changes.thumbnail.currentValue);
} }
} }
/**
* check if the Bitstream has any content than set the src
*/
checkThumbnail(thumbnail: Bitstream) {
if (
hasValue(thumbnail) &&
hasValue(thumbnail._links) &&
thumbnail._links.content.href
) {
this.src = thumbnail._links.content.href;
}
}
} }

View File

@@ -1,10 +1,10 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="dso.logo"> <ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="dso.logo"> <ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -19,6 +19,7 @@ import { TruncatableService } from '../../../truncatable/truncatable.service';
import { TruncatePipe } from '../../../utils/truncate.pipe'; import { TruncatePipe } from '../../../utils/truncate.pipe';
import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component'; import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
import { LinkService } from '../../../../core/cache/builders/link.service';
let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent; let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent;
let fixture: ComponentFixture<CollectionSearchResultGridElementComponent>; let fixture: ComponentFixture<CollectionSearchResultGridElementComponent>;
@@ -52,6 +53,9 @@ mockCollectionWithoutAbstract.indexableObject = Object.assign(new Collection(),
] ]
} }
}); });
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: mockCollectionWithAbstract
});
describe('CollectionSearchResultGridElementComponent', () => { describe('CollectionSearchResultGridElementComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
@@ -72,6 +76,7 @@ describe('CollectionSearchResultGridElementComponent', () => {
{ provide: DSOChangeAnalyzer, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamFormatDataService, useValue: {} }, { provide: BitstreamFormatDataService, useValue: {} },
{ provide: LinkService, useValue: linkService}
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]

View File

@@ -1,10 +1,14 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
import { hasNoValue, hasValue } from '../../../empty.util';
import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
@Component({ @Component({
selector: 'ds-collection-search-result-grid-element', selector: 'ds-collection-search-result-grid-element',
@@ -15,4 +19,28 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista
* Component representing a grid element for a collection search result * Component representing a grid element for a collection search result
*/ */
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement) @listableObjectComponent(CollectionSearchResult, ViewMode.GridElement)
export class CollectionSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> {} export class CollectionSearchResultGridElementComponent extends SearchResultGridElementComponent< CollectionSearchResult, Collection > {
private _dso: Collection;
constructor(
private linkService: LinkService,
protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService
) {
super(truncatableService, bitstreamDataService);
}
@Input() set dso(dso: Collection) {
this._dso = dso;
if (hasValue(this._dso) && hasNoValue(this._dso.logo)) {
this.linkService.resolveLink<Collection>(
this._dso,
followLink('logo')
);
}
}
get dso(): Collection {
return this._dso;
}
}

View File

@@ -1,10 +1,10 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="dso.logo"> <ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="dso.logo"> <ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -19,6 +19,7 @@ import { TruncatableService } from '../../../truncatable/truncatable.service';
import { TruncatePipe } from '../../../utils/truncate.pipe'; import { TruncatePipe } from '../../../utils/truncate.pipe';
import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component'; import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component';
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
import { LinkService } from '../../../../core/cache/builders/link.service';
let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent; let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent;
let fixture: ComponentFixture<CommunitySearchResultGridElementComponent>; let fixture: ComponentFixture<CommunitySearchResultGridElementComponent>;
@@ -52,6 +53,9 @@ mockCommunityWithoutAbstract.indexableObject = Object.assign(new Community(), {
] ]
} }
}); });
const linkService = jasmine.createSpyObj('linkService', {
resolveLink: mockCommunityWithAbstract
});
describe('CommunitySearchResultGridElementComponent', () => { describe('CommunitySearchResultGridElementComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
@@ -72,6 +76,7 @@ describe('CommunitySearchResultGridElementComponent', () => {
{ provide: DSOChangeAnalyzer, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamFormatDataService, useValue: {} }, { provide: BitstreamFormatDataService, useValue: {} },
{ provide: LinkService, useValue: linkService}
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]

View File

@@ -1,18 +1,46 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../../core/shared/community.model';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { hasNoValue, hasValue } from '../../../empty.util';
import { followLink } from '../../../utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-community-search-result-grid-element', selector: 'ds-community-search-result-grid-element',
styleUrls: ['../search-result-grid-element.component.scss', 'community-search-result-grid-element.component.scss'], styleUrls: [
templateUrl: 'community-search-result-grid-element.component.html' '../search-result-grid-element.component.scss',
'community-search-result-grid-element.component.scss',
],
templateUrl: 'community-search-result-grid-element.component.html',
}) })
/** /**
* Component representing a grid element for a community search result * Component representing a grid element for a community search result
*/ */
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement) @listableObjectComponent(CommunitySearchResult, ViewMode.GridElement)
export class CommunitySearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> { export class CommunitySearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult,Community> {
private _dso: Community;
constructor(
private linkService: LinkService,
protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService
) {
super(truncatableService, bitstreamDataService);
}
@Input() set dso(dso: Community) {
this._dso = dso;
if (hasValue(this._dso) && hasNoValue(this._dso.logo)) {
this.linkService.resolveLink<Community>(this._dso, followLink('logo'));
}
}
get dso(): Community {
return this._dso;
}
} }

View File

@@ -10,7 +10,7 @@
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> <ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail> </ds-grid-thumbnail>
</div> </div>
</span> </span>

View File

@@ -6,10 +6,10 @@ import { BitstreamDataService } from '../../../core/data/bitstream-data.service'
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Metadata } from '../../../core/shared/metadata.utils'; import { Metadata } from '../../../core/shared/metadata.utils';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { hasValue } from '../../empty.util'; import { hasValue } from '../../empty.util';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { TruncatableService } from '../../truncatable/truncatable.service'; import { TruncatableService } from '../../truncatable/truncatable.service';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
@Component({ @Component({
selector: 'ds-search-result-grid-element', selector: 'ds-search-result-grid-element',

View File

@@ -69,3 +69,24 @@ export function createPendingRemoteDataObject<T>(object?: T): RemoteData<T> {
export function createPendingRemoteDataObject$<T>(object?: T): Observable<RemoteData<T>> { export function createPendingRemoteDataObject$<T>(object?: T): Observable<RemoteData<T>> {
return observableOf(createPendingRemoteDataObject(object)); return observableOf(createPendingRemoteDataObject(object));
} }
/**
* Method to create a remote data object with no content
*/
export function createNoContentRemoteDataObject<T>(): RemoteData<T> {
return new RemoteData(
true,
true,
true,
null,
null,
204
);
}
/**
* Method to create a remote data object that has succeeded with no content, wrapped in an observable
*/
export function createNoContentRemoteDataObject$<T>(): Observable<RemoteData<T>> {
return observableOf(createNoContentRemoteDataObject());
}

View File

@@ -11,9 +11,10 @@ import { ItemPageConfig } from './item-page-config.interface';
import { CollectionPageConfig } from './collection-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface';
import { Theme } from './theme.inferface'; import { Theme } from './theme.inferface';
import {AuthConfig} from './auth-config.interfaces'; import {AuthConfig} from './auth-config.interfaces';
import { UIServerConfig } from './ui-server-config.interface';
export interface GlobalConfig extends Config { export interface GlobalConfig extends Config {
ui: ServerConfig; ui: UIServerConfig;
rest: ServerConfig; rest: ServerConfig;
production: boolean; production: boolean;
cache: CacheConfig; cache: CacheConfig;

View File

@@ -0,0 +1,14 @@
import { ServerConfig } from './server-config.interface';
/**
* Server configuration related to the UI.
*/
export class UIServerConfig extends ServerConfig {
// rateLimiter is used to limit the amount of requests a user is allowed make in an amount of time, in order to prevent overloading the server
rateLimiter?: {
windowMs: number;
max: number;
};
}

View File

@@ -13,6 +13,11 @@ export const environment: GlobalConfig = {
port: 4000, port: 4000,
// 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: '/', nameSpace: '/',
// The rateLimiter settings limit each IP to a "max" of 500 requests per "windowMs" (1 minute).
rateLimiter: {
windowMs: 1 * 60 * 1000, // 1 minute
max: 500 // limit each IP to 500 requests per windowMs
}
}, },
// The REST API server settings. // The REST API server settings.
// NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg. // NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg.

View File

@@ -19,6 +19,7 @@ export const environment: Partial<GlobalConfig> = {
port: 80, 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: '/angular-dspace', nameSpace: '/angular-dspace',
rateLimiter: undefined
}, },
// Caching settings // Caching settings
cache: { cache: {

View File

@@ -4084,6 +4084,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies: dependencies:
homedir-polyfill "^1.0.1" homedir-polyfill "^1.0.1"
express-rate-limit@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.1.3.tgz#656bacce3f093034976346958a0f0199902c9174"
integrity sha512-TINcxve5510pXj4n9/1AMupkj3iWxl3JuZaWhCdYDlZeoCPqweGZrxbrlqTCFb1CT5wli7s8e2SH/Qz2c9GorA==
express@4.16.2: express@4.16.2:
version "4.16.2" version "4.16.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"