mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'atmire/PR-1548-fix-group-page-lists' into w2p-94207_Fix-resource-policy-cache-issues
This commit is contained in:
16
.gitattributes
vendored
16
.gitattributes
vendored
@@ -1,2 +1,16 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# By default, auto detect text files and perform LF normalization
|
||||
# This ensures code is always checked in with LF line endings
|
||||
* text=auto
|
||||
|
||||
# JS and TS files must always use LF for Angular tools to work
|
||||
# Some Angular tools expect LF line endings, even on Windows.
|
||||
# This ensures Windows always checks out these files with LF line endings
|
||||
# We've copied many of these rules from https://github.com/angular/angular-cli/
|
||||
*.js eol=lf
|
||||
*.ts eol=lf
|
||||
*.json eol=lf
|
||||
*.json5 eol=lf
|
||||
*.css eol=lf
|
||||
*.scss eol=lf
|
||||
*.html eol=lf
|
||||
*.svg eol=lf
|
@@ -7,8 +7,9 @@ const appConfig: AppConfig = buildAppConfig();
|
||||
|
||||
/**
|
||||
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
||||
* Any CLI arguments given to this script are patched through to `ng serve` as well.
|
||||
*/
|
||||
child.spawn(
|
||||
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`,
|
||||
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')}`,
|
||||
{ stdio: 'inherit', shell: true }
|
||||
);
|
||||
|
0
scripts/sync-i18n-files.ts
Executable file → Normal file
0
scripts/sync-i18n-files.ts
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
<nav @slideHorizontal class="navbar navbar-dark p-0"
|
||||
<nav class="navbar navbar-dark p-0"
|
||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||
[@slideSidebar]="{
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
|
@@ -2,7 +2,7 @@ import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
import { slideSidebar } from '../../shared/animations/slide';
|
||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
@@ -18,7 +18,7 @@ import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
selector: 'ds-admin-sidebar',
|
||||
templateUrl: './admin-sidebar.component.html',
|
||||
styleUrls: ['./admin-sidebar.component.scss'],
|
||||
animations: [slideHorizontal, slideSidebar]
|
||||
animations: [slideSidebar]
|
||||
})
|
||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
/**
|
||||
|
0
src/app/app.module.ts
Executable file → Normal file
0
src/app/app.module.ts
Executable file → Normal file
@@ -7,57 +7,156 @@ import { TestScheduler } from 'rxjs/testing';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import objectContaining = jasmine.objectContaining;
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
describe(`AuthRequestService`, () => {
|
||||
let halService: HALEndpointService;
|
||||
let endpointURL: string;
|
||||
let requestID: string;
|
||||
let shortLivedToken: ShortLivedToken;
|
||||
let shortLivedTokenRD: RemoteData<ShortLivedToken>;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let service: AuthRequestService;
|
||||
let service;
|
||||
let testScheduler;
|
||||
|
||||
class TestAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
hes: HALEndpointService,
|
||||
rs: RequestService,
|
||||
rdbs: RemoteDataBuildService
|
||||
) {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
const status = new AuthStatus();
|
||||
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
class TestAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
hes: HALEndpointService,
|
||||
rs: RequestService,
|
||||
rdbs: RemoteDataBuildService
|
||||
) {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
|
||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||
endpointURL = 'https://rest.api/auth';
|
||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||
value: 'some-token'
|
||||
});
|
||||
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
}
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getEndpoint': cold('a', { a: endpointURL })
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'send': null
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
||||
});
|
||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||
endpointURL = 'https://rest.api/auth';
|
||||
requestID = 'requestID';
|
||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||
value: 'some-token'
|
||||
});
|
||||
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
||||
|
||||
service = new TestAuthRequestService(halService, requestService, rdbService);
|
||||
};
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getEndpoint': cold('a', { a: endpointURL })
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': requestID,
|
||||
'send': null,
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
||||
});
|
||||
|
||||
service = new TestAuthRequestService(halService, requestService, rdbService);
|
||||
|
||||
spyOn(service as any, 'fetchRequest').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject(status) }));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REST request methods', () => {
|
||||
let options: HttpOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
options = Object.create({});
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('should send a GET request to the right endpoint and return the auth status', () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
init(cold);
|
||||
|
||||
expectObservable(service.getRequest('method', options)).toBe('a', {
|
||||
a: objectContaining({ payload: status }),
|
||||
});
|
||||
flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
uuid: requestID,
|
||||
href: endpointURL + '/method',
|
||||
method: RestRequestMethod.GET,
|
||||
body: undefined,
|
||||
options,
|
||||
}));
|
||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send the request even if caller doesn\'t subscribe to the response', () => {
|
||||
testScheduler.run(({ cold, flush }) => {
|
||||
init(cold);
|
||||
|
||||
service.getRequest('method', options);
|
||||
flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
uuid: requestID,
|
||||
href: endpointURL + '/method',
|
||||
method: RestRequestMethod.GET,
|
||||
body: undefined,
|
||||
options,
|
||||
}));
|
||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST', () => {
|
||||
it('should send a POST request to the right endpoint and return the auth status', () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
init(cold);
|
||||
|
||||
expectObservable(service.postToEndpoint('method', { content: 'something' }, options)).toBe('a', {
|
||||
a: objectContaining({ payload: status }),
|
||||
});
|
||||
flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
uuid: requestID,
|
||||
href: endpointURL + '/method',
|
||||
method: RestRequestMethod.POST,
|
||||
body: { content: 'something' },
|
||||
options,
|
||||
}));
|
||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send the request even if caller doesn\'t subscribe to the response', () => {
|
||||
testScheduler.run(({ cold, flush }) => {
|
||||
init(cold);
|
||||
|
||||
service.postToEndpoint('method', { content: 'something' }, options);
|
||||
flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
uuid: requestID,
|
||||
href: endpointURL + '/method',
|
||||
method: RestRequestMethod.POST,
|
||||
body: { content: 'something' },
|
||||
options,
|
||||
}));
|
||||
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`getShortlivedToken`, () => {
|
||||
it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, map, switchMap, tap, take } from 'rxjs/operators';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
@@ -27,8 +27,13 @@ export abstract class AuthRequestService {
|
||||
) {
|
||||
}
|
||||
|
||||
protected fetchRequest(request: RestRequest, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
|
||||
return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid, ...linksToFollow).pipe(
|
||||
/**
|
||||
* Fetch the response to a request from the cache, once it's completed.
|
||||
* @param requestId the UUID of the request for which to retrieve the response
|
||||
* @protected
|
||||
*/
|
||||
protected fetchRequest(requestId: string, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
|
||||
return this.rdbService.buildFromRequestUUID<AuthStatus>(requestId, ...linksToFollow).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
}
|
||||
@@ -44,28 +49,48 @@ export abstract class AuthRequestService {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request to an authentication endpoint
|
||||
* @param method the method to send to (e.g. 'status')
|
||||
* @param body the data to send (optional)
|
||||
* @param options the HTTP options for the request
|
||||
*/
|
||||
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)),
|
||||
tap((request: PostRequest) => this.requestService.send(request)),
|
||||
mergeMap((request: PostRequest) => this.fetchRequest(request)),
|
||||
distinctUntilChanged());
|
||||
map((endpointURL: string) => new PostRequest(requestId, endpointURL, body, options)),
|
||||
take(1)
|
||||
).subscribe((request: PostRequest) => {
|
||||
this.requestService.send(request);
|
||||
});
|
||||
|
||||
return this.fetchRequest(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET request to an authentication endpoint
|
||||
* @param method the method to send to (e.g. 'status')
|
||||
* @param options the HTTP options for the request
|
||||
*/
|
||||
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)),
|
||||
tap((request: GetRequest) => this.requestService.send(request)),
|
||||
mergeMap((request: GetRequest) => this.fetchRequest(request, ...linksToFollow)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
map((endpointURL: string) => new GetRequest(requestId, endpointURL, undefined, options)),
|
||||
take(1)
|
||||
).subscribe((request: GetRequest) => {
|
||||
this.requestService.send(request);
|
||||
});
|
||||
|
||||
return this.fetchRequest(requestId, ...linksToFollow);
|
||||
}
|
||||
/**
|
||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
|
||||
/**
|
||||
* The auth state.
|
||||
@@ -251,6 +252,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
idle: false,
|
||||
});
|
||||
|
||||
case StoreActionTypes.REHYDRATE:
|
||||
return Object.assign({}, state, {
|
||||
blocking: true,
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@@ -15,10 +15,11 @@ import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resou
|
||||
import { COMMUNITY } from './community.resource-type';
|
||||
import { Community } from './community.model';
|
||||
import { ChildHALResource } from './child-hal-resource.model';
|
||||
import { HandleObject } from './handle-object.model';
|
||||
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class Collection extends DSpaceObject implements ChildHALResource {
|
||||
export class Collection extends DSpaceObject implements ChildHALResource, HandleObject {
|
||||
static type = COLLECTION;
|
||||
|
||||
/**
|
||||
|
@@ -11,10 +11,11 @@ import { COMMUNITY } from './community.resource-type';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { ChildHALResource } from './child-hal-resource.model';
|
||||
import { HandleObject } from './handle-object.model';
|
||||
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class Community extends DSpaceObject implements ChildHALResource {
|
||||
export class Community extends DSpaceObject implements ChildHALResource, HandleObject {
|
||||
static type = COMMUNITY;
|
||||
|
||||
/**
|
||||
|
8
src/app/core/shared/handle-object.model.ts
Normal file
8
src/app/core/shared/handle-object.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Interface representing an object in DSpace that contains a handle
|
||||
*/
|
||||
export interface HandleObject {
|
||||
|
||||
handle: string;
|
||||
|
||||
}
|
@@ -23,13 +23,14 @@ import { BITSTREAM } from './bitstream.resource-type';
|
||||
import { Bitstream } from './bitstream.model';
|
||||
import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
|
||||
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
|
||||
import { HandleObject } from './handle-object.model';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Item
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class Item extends DSpaceObject implements ChildHALResource {
|
||||
export class Item extends DSpaceObject implements ChildHALResource, HandleObject {
|
||||
static type = ITEM;
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,7 @@ import { inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { authReducer } from './core/auth/auth.reducer';
|
||||
import { storeModuleConfig } from './app.reducer';
|
||||
import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock';
|
||||
@@ -31,6 +31,7 @@ import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||
import objectContaining = jasmine.objectContaining;
|
||||
import createSpyObj = jasmine.createSpyObj;
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
let spy: SpyObj<any>;
|
||||
|
||||
@@ -124,6 +125,15 @@ describe('InitService', () => {
|
||||
let metadataServiceSpy;
|
||||
let breadcrumbsServiceSpy;
|
||||
|
||||
const BLOCKING = {
|
||||
t: { core: { auth: { blocking: true } } },
|
||||
f: { core: { auth: { blocking: false } } },
|
||||
};
|
||||
const BOOLEAN = {
|
||||
t: true,
|
||||
f: false,
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [
|
||||
'initCorrelationId',
|
||||
@@ -182,6 +192,18 @@ describe('InitService', () => {
|
||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('authenticationReady', () => {
|
||||
it('should emit & complete the first time auth is unblocked', () => {
|
||||
getTestScheduler().run(({ cold, expectObservable }) => {
|
||||
TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) });
|
||||
const service = TestBed.inject(InitService);
|
||||
|
||||
// @ts-ignore
|
||||
expectObservable(service.authenticationReady$()).toBe('------(f|)', BOOLEAN);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Store } from '@ngrx/store';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||
import { APP_INITIALIZER, Inject, Provider, Type } from '@angular/core';
|
||||
@@ -20,6 +20,9 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||
import { distinctUntilChanged, find } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Performs the initialization of the app.
|
||||
@@ -186,4 +189,16 @@ export abstract class InitService {
|
||||
this.breadcrumbsService.listenForRouteChanges();
|
||||
this.themeService.listenForRouteChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits once authentication is ready (no longer blocking)
|
||||
* @protected
|
||||
*/
|
||||
protected authenticationReady$(): Observable<boolean> {
|
||||
return this.store.pipe(
|
||||
select(isAuthenticationBlocking),
|
||||
distinctUntilChanged(),
|
||||
find((b: boolean) => b === false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -16,24 +16,24 @@ import { filter, find, map, take } from 'rxjs/operators';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
ThemedCreateCommunityParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
ThemedCreateCollectionParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
ThemedCreateItemParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
ThemedEditCommunitySelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
ThemedEditCollectionSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
ThemedEditItemSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import {
|
||||
ExportMetadataSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
@@ -188,7 +188,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -201,7 +201,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -214,7 +214,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -263,7 +263,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
this.modalService.open(ThemedEditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -276,7 +276,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
this.modalService.open(ThemedEditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -289,7 +289,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
this.modalService.open(ThemedEditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
@@ -1,53 +1,99 @@
|
||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
||||
<div>
|
||||
<button class="btn btn-lg btn-success " routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
||||
</div>
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
||||
id: process?.processId,
|
||||
name: process?.scriptName
|
||||
} }}</h2>
|
||||
</div>
|
||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||
<div>{{ process?.scriptName }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
|
||||
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments"
|
||||
[title]="'process.detail.arguments'">
|
||||
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||
[title]="'process.detail.output-files'">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
</ds-process-detail-field>
|
||||
</div>
|
||||
|
||||
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
|
||||
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time"
|
||||
[title]="'process.detail.start-time' | translate">
|
||||
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
|
||||
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time"
|
||||
[title]="'process.detail.end-time' | translate">
|
||||
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
|
||||
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status"
|
||||
[title]="'process.detail.status' | translate">
|
||||
<div>{{ process.processStatus }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()">
|
||||
{{ 'process.detail.logs.button' | translate }}
|
||||
</button>
|
||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||
<pre class="font-weight-bold text-secondary bg-light p-3"
|
||||
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
||||
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton"
|
||||
class="btn btn-primary" (click)="showProcessOutputLogs()">
|
||||
{{ 'process.detail.logs.button' | translate }}
|
||||
</button>
|
||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading"
|
||||
message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||
<pre class="font-weight-bold text-secondary bg-light p-3"
|
||||
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
||||
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
||||
&& !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output">
|
||||
{{ 'process.detail.logs.none' | translate }}
|
||||
</p>
|
||||
{{ 'process.detail.logs.none' | translate }}
|
||||
</p>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<ds-process-detail-field id="process-actions" [title]="'process.detail.actions'">
|
||||
<button class="btn btn-success mr-2" routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i
|
||||
class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
||||
<button *ngIf="isProcessFinished(process)" id="delete" class="btn btn-danger"
|
||||
(click)="openDeleteModal(deleteModal)">
|
||||
<i class="fas fa-trash pr-2"></i>{{ 'process.detail.delete.button' | translate }}
|
||||
</button>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #deleteModal >
|
||||
|
||||
<div *ngVar="(processRD$ | async)?.payload as process">
|
||||
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h4>{{'process.detail.delete.header' | translate }}</h4>
|
||||
</div>
|
||||
<button type="button" class="close"
|
||||
(click)="closeModal()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div>{{'process.detail.delete.body' | translate }}</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary mr-2" (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||
<button id="delete-confirm" class="btn btn-danger"
|
||||
(click)="deleteProcess(process)">{{ 'process.detail.delete.confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
|
@@ -19,15 +19,23 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||
import { Process } from '../processes/process.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||
|
||||
describe('ProcessDetailComponent', () => {
|
||||
let component: ProcessDetailComponent;
|
||||
@@ -44,6 +52,11 @@ describe('ProcessDetailComponent', () => {
|
||||
|
||||
let processOutput;
|
||||
|
||||
let modalService;
|
||||
let notificationsService;
|
||||
|
||||
let router;
|
||||
|
||||
function init() {
|
||||
processOutput = 'Process Started';
|
||||
process = Object.assign(new Process(), {
|
||||
@@ -93,7 +106,8 @@ describe('ProcessDetailComponent', () => {
|
||||
}
|
||||
});
|
||||
processService = jasmine.createSpyObj('processService', {
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||
delete: createSuccessfulRemoteDataObject$(null)
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||
@@ -104,13 +118,23 @@ describe('ProcessDetailComponent', () => {
|
||||
httpClient = jasmine.createSpyObj('httpClient', {
|
||||
get: observableOf(processOutput)
|
||||
});
|
||||
|
||||
modalService = jasmine.createSpyObj('modalService', {
|
||||
open: {}
|
||||
});
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl:{}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
@@ -121,6 +145,9 @@ describe('ProcessDetailComponent', () => {
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: HttpClient, useValue: httpClient },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -207,4 +234,34 @@ describe('ProcessDetailComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDeleteModal', () => {
|
||||
it('should open the modal', () => {
|
||||
component.openDeleteModal({});
|
||||
expect(modalService.open).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProcess', () => {
|
||||
it('should delete the process and navigate back to the overview page on success', () => {
|
||||
spyOn(component, 'closeModal');
|
||||
component.deleteProcess(process);
|
||||
|
||||
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(component.closeModal).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessListRoute());
|
||||
});
|
||||
it('should delete the process and not navigate on error', () => {
|
||||
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
spyOn(component, 'closeModal');
|
||||
|
||||
component.deleteProcess(process);
|
||||
|
||||
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(component.closeModal).not.toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -12,8 +12,9 @@ import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteData
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
@@ -21,6 +22,10 @@ import { hasValue } from '../../shared/empty.util';
|
||||
import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { Process } from '../processes/process.model';
|
||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-process-detail',
|
||||
@@ -71,6 +76,11 @@ export class ProcessDetailComponent implements OnInit {
|
||||
*/
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
protected modalRef: NgbModalRef;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
@@ -78,7 +88,11 @@ export class ProcessDetailComponent implements OnInit {
|
||||
protected nameService: DSONameService,
|
||||
private zone: NgZone,
|
||||
protected authService: AuthService,
|
||||
protected http: HttpClient) {
|
||||
protected http: HttpClient,
|
||||
protected modalService: NgbModal,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,4 +186,36 @@ export class ProcessDetailComponent implements OnInit {
|
||||
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current process
|
||||
* @param process
|
||||
*/
|
||||
deleteProcess(process: Process) {
|
||||
this.processService.delete(process.processId).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('process.detail.delete.success'));
|
||||
this.closeModal();
|
||||
this.router.navigateByUrl(getProcessListRoute());
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('process.detail.delete.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given modal.
|
||||
* @param content - the modal content.
|
||||
*/
|
||||
openDeleteModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
closeModal() {
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,149 @@
|
||||
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||
import { waitForAsync } from '@angular/core/testing';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||
|
||||
describe('ProcessBulkDeleteService', () => {
|
||||
|
||||
let service: ProcessBulkDeleteService;
|
||||
let processDataService;
|
||||
let notificationsService;
|
||||
let mockTranslateService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
processDataService = jasmine.createSpyObj('processDataService', {
|
||||
delete: createSuccessfulRemoteDataObject$(null)
|
||||
});
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
mockTranslateService = getMockTranslateService();
|
||||
service = new ProcessBulkDeleteService(processDataService, notificationsService, mockTranslateService);
|
||||
}));
|
||||
|
||||
describe('toggleDelete', () => {
|
||||
it('should add a new value to the processesToDelete list when not yet present', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||
});
|
||||
it('should remove a value from the processesToDelete list when already present', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||
|
||||
service.toggleDelete('test-id-1');
|
||||
expect(service.processesToDelete).toEqual(['test-id-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToBeDeleted', () => {
|
||||
it('should return true when the provided process id is present in the list', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.isToBeDeleted('test-id-1')).toBeTrue();
|
||||
});
|
||||
it('should return false when the provided process id is not present in the list', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.isToBeDeleted('test-id-3')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAllProcesses', () => {
|
||||
it('should clear the list of to be deleted processes', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||
|
||||
service.clearAllProcesses();
|
||||
expect(service.processesToDelete).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe('getAmountOfSelectedProcesses', () => {
|
||||
it('should return the amount of the currently selected processes for deletion', () => {
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.getAmountOfSelectedProcesses()).toEqual(2);
|
||||
});
|
||||
});
|
||||
describe('isProcessing$', () => {
|
||||
it('should return a behavior subject containing whether a delete is currently processing or not', () => {
|
||||
const result = service.isProcessing$();
|
||||
expect(result.getValue()).toBeFalse();
|
||||
|
||||
result.next(true);
|
||||
expect(result.getValue()).toBeTrue();
|
||||
});
|
||||
});
|
||||
describe('hasSelected', () => {
|
||||
it('should return if the list of selected processes has values', () => {
|
||||
expect(service.hasSelected()).toBeFalse();
|
||||
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
|
||||
expect(service.hasSelected()).toBeTrue();
|
||||
});
|
||||
});
|
||||
describe('deleteSelectedProcesses', () => {
|
||||
it('should delete all selected processes, show an error for each failed one and a notification at the end with the amount of succeeded deletions', () => {
|
||||
(processDataService.delete as jasmine.Spy).and.callFake((processId: string) => {
|
||||
if (processId.includes('error')) {
|
||||
return createFailedRemoteDataObject$();
|
||||
} else {
|
||||
return createSuccessfulRemoteDataObject$(null);
|
||||
}
|
||||
});
|
||||
|
||||
service.toggleDelete('test-id-1');
|
||||
service.toggleDelete('test-id-2');
|
||||
service.toggleDelete('error-id-3');
|
||||
service.toggleDelete('test-id-4');
|
||||
service.toggleDelete('error-id-5');
|
||||
service.toggleDelete('error-id-6');
|
||||
service.toggleDelete('test-id-7');
|
||||
|
||||
|
||||
service.deleteSelectedProcesses();
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('test-id-1');
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('test-id-2');
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('error-id-3');
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-3'});
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('test-id-4');
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('error-id-5');
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-5'});
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('error-id-6');
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-6'});
|
||||
|
||||
|
||||
expect(processDataService.delete).toHaveBeenCalledWith('test-id-7');
|
||||
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.success', {count: 4});
|
||||
|
||||
expect(service.processesToDelete).toEqual(['error-id-3', 'error-id-5', 'error-id-6']);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Process } from '../processes/process.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { BehaviorSubject, count, from } from 'rxjs';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { concatMap, filter, tap } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Service to facilitate removing processes in bulk.
|
||||
*/
|
||||
export class ProcessBulkDeleteService {
|
||||
|
||||
/**
|
||||
* Array to track the processes to be deleted
|
||||
*/
|
||||
processesToDelete: string[] = [];
|
||||
|
||||
/**
|
||||
* Behavior subject to track whether the delete is processing
|
||||
* @protected
|
||||
*/
|
||||
protected isProcessingBehaviorSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
constructor(
|
||||
protected processDataService: ProcessDataService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a process id to/from the list
|
||||
* If the id is already present it will be removed, otherwise it will be added.
|
||||
*
|
||||
* @param processId - The process id to add or remove
|
||||
*/
|
||||
toggleDelete(processId: string) {
|
||||
if (this.isToBeDeleted(processId)) {
|
||||
this.processesToDelete.splice(this.processesToDelete.indexOf(processId), 1);
|
||||
} else {
|
||||
this.processesToDelete.push(processId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided process id is present in the to be deleted list
|
||||
* @param processId
|
||||
*/
|
||||
isToBeDeleted(processId: string) {
|
||||
return this.processesToDelete.includes(processId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the list of processes to be deleted
|
||||
*/
|
||||
clearAllProcesses() {
|
||||
this.processesToDelete.splice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of processes selected for deletion
|
||||
*/
|
||||
getAmountOfSelectedProcesses() {
|
||||
return this.processesToDelete.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a behavior subject to indicate whether the bulk delete is processing
|
||||
*/
|
||||
isProcessing$() {
|
||||
return this.isProcessingBehaviorSubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there currently are values selected for deletion
|
||||
*/
|
||||
hasSelected(): boolean {
|
||||
return isNotEmpty(this.processesToDelete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all selected processes one by one
|
||||
* When the deletion for a process fails, an error notification will be shown with the process id,
|
||||
* but it will continue deleting the other processes.
|
||||
* At the end it will show a notification stating the amount of successful deletes
|
||||
* The successfully deleted processes will be removed from the list of selected values, the failed ones will be retained.
|
||||
*/
|
||||
deleteSelectedProcesses() {
|
||||
this.isProcessingBehaviorSubject.next(true);
|
||||
|
||||
from([...this.processesToDelete]).pipe(
|
||||
concatMap((processId) => {
|
||||
return this.processDataService.delete(processId).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
tap((rd: RemoteData<Process>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get('process.bulk.delete.error.head'), this.translateService.get('process.bulk.delete.error.body', {processId: processId}));
|
||||
} else {
|
||||
this.toggleDelete(processId);
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
filter((rd: RemoteData<Process>) => rd.hasSucceeded),
|
||||
count(),
|
||||
).subscribe((value) => {
|
||||
this.notificationsService.success(this.translateService.get('process.bulk.delete.success', {count: value}));
|
||||
this.isProcessingBehaviorSubject.next(false);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,7 +1,19 @@
|
||||
<div class="container">
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
||||
<button class="btn btn-lg btn-success " routerLink="/processes/new"><i class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
|
||||
(click)="processBulkDeleteService.clearAllProcesses()"><i
|
||||
class="fas fa-undo pr-2"></i>{{'process.overview.delete.clear' | translate }}
|
||||
</button>
|
||||
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-danger mr-2"
|
||||
(click)="openDeleteModal(deleteModal)"><i
|
||||
class="fas fa-trash pr-2"></i>{{'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||
</button>
|
||||
<button class="btn btn-success" routerLink="/processes/new"><i
|
||||
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||
|
||||
</div>
|
||||
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="pageConfig"
|
||||
@@ -19,19 +31,61 @@
|
||||
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
||||
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
||||
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
||||
<th scope="col">{{'process.overview.table.actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page">
|
||||
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page"
|
||||
[class.table-danger]="processBulkDeleteService.isToBeDeleted(process.processId)">
|
||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
||||
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
||||
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
||||
<td>{{process.processStatus}}</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-danger"
|
||||
(click)="processBulkDeleteService.toggleDelete(process.processId)"><i
|
||||
class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
|
||||
<ng-template #deleteModal>
|
||||
|
||||
<div>
|
||||
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h4>{{'process.overview.delete.header' | translate }}</h4>
|
||||
</div>
|
||||
<button type="button" class="close"
|
||||
(click)="closeModal()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div *ngIf="!(processBulkDeleteService.isProcessing$() |async)">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
|
||||
<div *ngIf="processBulkDeleteService.isProcessing$() |async" class="alert alert-info">
|
||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||
<button id="delete-confirm" class="btn btn-danger"
|
||||
[disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ProcessOverviewComponent } from './process-overview.component';
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
@@ -13,11 +13,11 @@ import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
describe('ProcessOverviewComponent', () => {
|
||||
let component: ProcessOverviewComponent;
|
||||
@@ -30,6 +30,9 @@ describe('ProcessOverviewComponent', () => {
|
||||
let processes: Process[];
|
||||
let ePerson: EPerson;
|
||||
|
||||
let processBulkDeleteService;
|
||||
let modalService;
|
||||
|
||||
const pipe = new DatePipe('en-US');
|
||||
|
||||
function init() {
|
||||
@@ -80,6 +83,29 @@ describe('ProcessOverviewComponent', () => {
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
|
||||
clearAllProcesses: {},
|
||||
deleteSelectedProcesses: {},
|
||||
isProcessing$: new BehaviorSubject(false),
|
||||
hasSelected: true,
|
||||
isToBeDeleted: true,
|
||||
toggleDelete: {},
|
||||
getAmountOfSelectedProcesses: 5
|
||||
|
||||
});
|
||||
|
||||
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
|
||||
if (id === 2) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
modalService = jasmine.createSpyObj('modalService', {
|
||||
open: {}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -90,7 +116,9 @@ describe('ProcessOverviewComponent', () => {
|
||||
providers: [
|
||||
{ provide: ProcessDataService, useValue: processService },
|
||||
{ provide: EPersonDataService, useValue: ePersonService },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -154,5 +182,71 @@ describe('ProcessOverviewComponent', () => {
|
||||
expect(el.textContent).toContain(processes[index].processStatus);
|
||||
});
|
||||
});
|
||||
it('should display a delete button in the seventh column', () => {
|
||||
rowElements.forEach((rowElement, index) => {
|
||||
const el = rowElement.query(By.css('td:nth-child(7)'));
|
||||
expect(el.nativeElement.innerHTML).toContain('fas fa-trash');
|
||||
|
||||
el.query(By.css('button')).triggerEventHandler('click', null);
|
||||
expect(processBulkDeleteService.toggleDelete).toHaveBeenCalledWith(processes[index].processId);
|
||||
});
|
||||
});
|
||||
it('should indicate a row that has been selected for deletion', () => {
|
||||
const deleteRow = fixture.debugElement.query(By.css('.table-danger'));
|
||||
expect(deleteRow.nativeElement.innerHTML).toContain('/processes/' + processes[1].processId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overview buttons', () => {
|
||||
it('should show a button to clear selected processes when there are selected processes', () => {
|
||||
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||
expect(clearButton.nativeElement.innerHTML).toContain('process.overview.delete.clear');
|
||||
|
||||
clearButton.triggerEventHandler('click', null);
|
||||
expect(processBulkDeleteService.clearAllProcesses).toHaveBeenCalled();
|
||||
});
|
||||
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||
expect(clearButton).toBeNull();
|
||||
});
|
||||
it('should show a button to open the delete modal when there are selected processes', () => {
|
||||
spyOn(component, 'openDeleteModal');
|
||||
|
||||
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||
expect(deleteButton.nativeElement.innerHTML).toContain('process.overview.delete');
|
||||
|
||||
deleteButton.triggerEventHandler('click', null);
|
||||
expect(component.openDeleteModal).toHaveBeenCalled();
|
||||
});
|
||||
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDeleteModal', () => {
|
||||
it('should open the modal', () => {
|
||||
component.openDeleteModal({});
|
||||
expect(modalService.open).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSelected', () => {
|
||||
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
|
||||
spyOn(component, 'closeModal');
|
||||
spyOn(component, 'setProcesses');
|
||||
|
||||
component.deleteSelected();
|
||||
|
||||
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
|
||||
expect(component.closeModal).toHaveBeenCalled();
|
||||
expect(component.setProcesses).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { Process } from '../processes/process.model';
|
||||
@@ -11,6 +11,9 @@ import { map, switchMap } from 'rxjs/operators';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-process-overview',
|
||||
@@ -19,7 +22,7 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
/**
|
||||
* Component displaying a list of all processes in a paginated table
|
||||
*/
|
||||
export class ProcessOverviewComponent implements OnInit {
|
||||
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* List of all processes
|
||||
@@ -46,13 +49,22 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
*/
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||
|
||||
processesToDelete: string[] = [];
|
||||
private modalRef: any;
|
||||
|
||||
isProcessingSub: Subscription;
|
||||
|
||||
constructor(protected processService: ProcessDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected ePersonService: EPersonDataService) {
|
||||
protected ePersonService: EPersonDataService,
|
||||
protected modalService: NgbModal,
|
||||
public processBulkDeleteService: ProcessBulkDeleteService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setProcesses();
|
||||
this.processBulkDeleteService.clearAllProcesses();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +72,7 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
*/
|
||||
setProcesses() {
|
||||
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||
switchMap((config) => this.processService.findAll(config))
|
||||
switchMap((config) => this.processService.findAll(config, true, false))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,8 +86,46 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
map((eperson: EPerson) => eperson.name)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.pageConfig.id);
|
||||
if (hasValue(this.isProcessingSub)) {
|
||||
this.isProcessingSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given modal.
|
||||
* @param content - the modal content.
|
||||
*/
|
||||
openDeleteModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
closeModal() {
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the previously selected processes using the processBulkDeleteService
|
||||
* After the deletion has started, subscribe to the isProcessing$ and when it is set
|
||||
* to false after the processing is done, close the modal and reinitialise the processes
|
||||
*/
|
||||
deleteSelected() {
|
||||
this.processBulkDeleteService.deleteSelectedProcesses();
|
||||
|
||||
if (hasValue(this.isProcessingSub)) {
|
||||
this.isProcessingSub.unsubscribe();
|
||||
}
|
||||
this.isProcessingSub = this.processBulkDeleteService.isProcessing$()
|
||||
.subscribe((isProcessing) => {
|
||||
if (!isProcessing) {
|
||||
this.closeModal();
|
||||
this.setProcesses();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@@ -71,7 +71,8 @@ export class RootComponent implements OnInit {
|
||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
||||
.pipe(
|
||||
map(([collapsed, mobile]) => collapsed || mobile)
|
||||
map(([collapsed, mobile]) => collapsed || mobile),
|
||||
startWith(true),
|
||||
);
|
||||
|
||||
if (this.router.url === getPageInternalServerErrorRoute()) {
|
||||
|
@@ -10,13 +10,6 @@ export const slide = trigger('slide', [
|
||||
transition('expanded <=> collapsed', animate(250))
|
||||
]);
|
||||
|
||||
export const slideHorizontal = trigger('slideHorizontal', [
|
||||
state('void', style({ width: 0 })),
|
||||
state('*', style({ width: '*' })),
|
||||
transition(':enter', [animate('200ms')]),
|
||||
transition(':leave', [animate('200ms')])
|
||||
]);
|
||||
|
||||
export const slideMobileNav = trigger('slideMobileNav', [
|
||||
|
||||
state('expanded', style({ height: '100vh' })),
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CreateCollectionParentSelectorComponent} from './create-collection-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateCollectionParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-collection-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateCollectionParentSelectorComponent
|
||||
extends ThemedComponent<CreateCollectionParentSelectorComponent> {
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'CreateCollectionParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-collection-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CreateCommunityParentSelectorComponent} from './create-community-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateCommunityParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-community-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateCommunityParentSelectorComponent
|
||||
extends ThemedComponent<CreateCommunityParentSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CreateCommunityParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-community-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {CreateItemParentSelectorComponent} from './create-item-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateItemParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-item-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateItemParentSelectorComponent
|
||||
extends ThemedComponent<CreateItemParentSelectorComponent> {
|
||||
@Input() entityType: string;
|
||||
|
||||
protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType'];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'CreateItemParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-item-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditCollectionSelectorComponent} from './edit-collection-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditCollectionSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-collection-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditCollectionSelectorComponent
|
||||
extends ThemedComponent<EditCollectionSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditCollectionSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-collection-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditCommunitySelectorComponent} from './edit-community-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditCommunitySelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-community-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditCommunitySelectorComponent
|
||||
extends ThemedComponent<EditCommunitySelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditCommunitySelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-community-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditItemSelectorComponent} from './edit-item-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditItemSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-item-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditItemSelectorComponent
|
||||
extends ThemedComponent<EditItemSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditItemSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-item-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -124,12 +124,21 @@ import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.c
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateCommunityParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateItemParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateCollectionParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import {
|
||||
CommunitySearchResultListElementComponent
|
||||
} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
||||
@@ -139,12 +148,21 @@ import {
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import {
|
||||
ThemedEditItemSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import {
|
||||
ThemedEditCommunitySelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import {
|
||||
ThemedEditCollectionSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import {
|
||||
ItemListPreviewComponent
|
||||
} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
||||
@@ -395,11 +413,17 @@ const COMPONENTS = [
|
||||
DsoInputSuggestionsComponent,
|
||||
DSOSelectorComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
ThemedCreateCommunityParentSelectorComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
ThemedCreateCollectionParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
ThemedCreateItemParentSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
ThemedEditCommunitySelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
ThemedEditCollectionSelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
ThemedEditItemSelectorComponent,
|
||||
CommunitySearchResultListElementComponent,
|
||||
CollectionSearchResultListElementComponent,
|
||||
BrowseByComponent,
|
||||
@@ -491,11 +515,17 @@ const ENTRY_COMPONENTS = [
|
||||
StartsWithDateComponent,
|
||||
StartsWithTextComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
ThemedCreateCommunityParentSelectorComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
ThemedCreateCollectionParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
ThemedCreateItemParentSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
ThemedEditCommunitySelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
ThemedEditCollectionSelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
ThemedEditItemSelectorComponent,
|
||||
PlainTextMetadataListElementComponent,
|
||||
ItemMetadataListElementComponent,
|
||||
MetadataRepresentationListElementComponent,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store';
|
||||
import { Injectable, Inject, Injector } from '@angular/core';
|
||||
import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store';
|
||||
import { BehaviorSubject, EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||
import { ThemeState } from './theme.reducer';
|
||||
import { SetThemeAction, ThemeActionTypes } from './theme.actions';
|
||||
@@ -53,12 +53,13 @@ export class ThemeService {
|
||||
private store: Store<ThemeState>,
|
||||
private linkService: LinkService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
protected injector: Injector,
|
||||
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
|
||||
private router: Router,
|
||||
@Inject(DOCUMENT) private document: any,
|
||||
) {
|
||||
// Create objects from the theme configs in the environment file
|
||||
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig));
|
||||
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig, injector));
|
||||
this.hasDynamicTheme = environment.themes.some((themeConfig: any) =>
|
||||
hasValue(themeConfig.regex) ||
|
||||
hasValue(themeConfig.handle) ||
|
||||
|
@@ -24,9 +24,12 @@
|
||||
</ds-viewable-collection>
|
||||
<ds-themed-loading *ngIf="(isLoading$ | async)"
|
||||
message="{{'loading.search-results' | translate}}"></ds-themed-loading>
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" data-test="empty-external-entry-list">
|
||||
<ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
|
||||
</div>
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD.statusCode === 500" data-test="empty-external-error-500">
|
||||
<ds-alert [type]="'alert-info'">{{ 'search.results.response.500' | translate }}</ds-alert>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
|
||||
|
@@ -19,9 +19,15 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
|
||||
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('SubmissionImportExternalComponent test suite', () => {
|
||||
let comp: SubmissionImportExternalComponent;
|
||||
@@ -44,7 +50,8 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
beforeEach(waitForAsync (() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
TranslateModule.forRoot(),
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
SubmissionImportExternalComponent,
|
||||
@@ -177,6 +184,326 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle backend response for search query', () => {
|
||||
const paginatedData: any = {
|
||||
'timeCompleted': 1657009282990,
|
||||
'msToLive': 900000,
|
||||
'lastUpdated': 1657009282990,
|
||||
'state': 'Success',
|
||||
'errorMessage': null,
|
||||
'payload': {
|
||||
'type': {
|
||||
'value': 'paginated-list'
|
||||
},
|
||||
'pageInfo': {
|
||||
'elementsPerPage': 10,
|
||||
'totalElements': 11971608,
|
||||
'totalPages': 1197161,
|
||||
'currentPage': 1
|
||||
},
|
||||
'_links': {
|
||||
'first': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=0&size=10&sort=id,asc'
|
||||
},
|
||||
'self': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?sort=id,ASC&page=0&size=10&query=test'
|
||||
},
|
||||
'next': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1&size=10&sort=id,asc'
|
||||
},
|
||||
'last': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1197160&size=10&sort=id,asc'
|
||||
},
|
||||
'page': [
|
||||
{
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||
}
|
||||
]
|
||||
},
|
||||
'page': [
|
||||
{
|
||||
'id': '2-s2.0-85130258665',
|
||||
'type': 'externalSourceEntry',
|
||||
'display': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'externalSource': 'scopus',
|
||||
'metadata': {
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
'uuid': 'cbceba09-4c12-4968-ab02-2f77a985b422',
|
||||
'language': null,
|
||||
'value': 'Silva I.M.M.',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
'uuid': 'e8d3c306-ce21-43e2-8a80-5f257cc3b7ea',
|
||||
'language': null,
|
||||
'value': '2024-01-01',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
'uuid': 'c9ee4076-c602-4c1d-ab1a-60bbdd0dd511',
|
||||
'language': null,
|
||||
'value': 'This systematic review integrates the data available in the literature regarding the biological activities of the extracts of endophytic fungi isolated from Annona muricata and their secondary metabolites. The search was performed using four electronic databases, and studies’ quality was evaluated using an adapted assessment tool. The initial database search yielded 436 results; ten studies were selected for inclusion. The leaf was the most studied part of the plant (in nine studies); Periconia sp. was the most tested fungus (n = 4); the most evaluated biological activity was anticancer (n = 6), followed by antiviral (n = 3). Antibacterial, antifungal, and antioxidant activities were also tested. Terpenoids or terpenoid hybrid compounds were the most abundant chemical metabolites. Phenolic compounds, esters, alkaloids, saturated and unsaturated fatty acids, aromatic compounds, and peptides were also reported. The selected studies highlighted the biotechnological potentiality of the endophytic fungi extracts from A. muricata. Consequently, it can be considered a promising source of biological compounds with antioxidant effects and active against different microorganisms and cancer cells. Further research is needed involving different plant tissues, other microorganisms, such as SARS-CoV-2, and different cancer cells.',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.doi': [
|
||||
{
|
||||
'uuid': '95ec26be-c1b4-4c4a-b12d-12421a4f181d',
|
||||
'language': null,
|
||||
'value': '10.1590/1519-6984.259525',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.pmid': [
|
||||
{
|
||||
'uuid': 'd6913cd6-1007-4013-b486-3f07192bc739',
|
||||
'language': null,
|
||||
'value': '35588520',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.scopus': [
|
||||
{
|
||||
'uuid': '6386a1f6-84ba-431d-a583-e16d19af8db0',
|
||||
'language': null,
|
||||
'value': '2-s2.0-85130258665',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.grantno': [
|
||||
{
|
||||
'uuid': 'bcafd7b0-827d-4abb-8608-95dc40a8e58a',
|
||||
'language': null,
|
||||
'value': 'undefined',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.ispartof': [
|
||||
{
|
||||
'uuid': '680819c8-c143-405f-9d09-f84d2d5cd338',
|
||||
'language': null,
|
||||
'value': 'Brazilian Journal of Biology',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.ispartofseries': [
|
||||
{
|
||||
'uuid': '06634104-127b-44f6-9dcc-efae24b74bd1',
|
||||
'language': null,
|
||||
'value': 'Brazilian Journal of Biology',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.issn': [
|
||||
{
|
||||
'uuid': '5f6cce46-2538-49e9-8ed0-a3988dcac6c5',
|
||||
'language': null,
|
||||
'value': '15196984',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.subject': [
|
||||
{
|
||||
'uuid': '0b6fbc77-de54-4f4a-b317-3d74a429f22a',
|
||||
'language': null,
|
||||
'value': 'biological products | biotechnology | mycology | soursop',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
'uuid': '4c0fa3d3-1a8c-4302-a772-4a4d0408df35',
|
||||
'language': null,
|
||||
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
'uuid': '5b6e0337-6f79-4574-a720-536816d1dc6e',
|
||||
'language': null,
|
||||
'value': 'Journal',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oaire.citation.volume': [
|
||||
{
|
||||
'uuid': 'b88b0246-61a9-4aca-917f-68afc8ead7d8',
|
||||
'language': null,
|
||||
'value': '84',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oairecerif.affiliation.orgunit': [
|
||||
{
|
||||
'uuid': '487c0fbc-3622-4cc7-a5fa-4edf780c6a21',
|
||||
'language': null,
|
||||
'value': 'Universidade Federal do Reconcavo da Bahia',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oairecerif.citation.number': [
|
||||
{
|
||||
'uuid': '90808bdd-f456-4ba3-91aa-b82fb3c453f6',
|
||||
'language': null,
|
||||
'value': 'e259525',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'person.identifier.orcid': [
|
||||
{
|
||||
'uuid': 'e533d0d2-cf26-4c3e-b5ae-cabf497dfb6b',
|
||||
'language': null,
|
||||
'value': '#PLACEHOLDER_PARENT_METADATA_VALUE#',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'person.identifier.scopus-author-id': [
|
||||
{
|
||||
'uuid': '4faf0be5-0226-4d4f-92a0-938397c4ec02',
|
||||
'language': null,
|
||||
'value': '42561627000',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
]
|
||||
},
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'statusCode': 200
|
||||
};
|
||||
const errorObj = {
|
||||
errorMessage: 'Http failure response for ' +
|
||||
'https://example.com/server/api/integration/externalsources/pubmed/entries?sort=id,ASC&page=0&size=10&query=test: 500 OK',
|
||||
statusCode: 500,
|
||||
timeCompleted: 1656950434666,
|
||||
errors: [{
|
||||
'message': 'Internal Server Error', 'paths': ['/server/api/integration/externalsources/pubmed/entries']
|
||||
}]
|
||||
};
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubmissionImportExternalComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
comp = null;
|
||||
compAsAny = null;
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 200 response with valid content', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(paginatedData.payload));
|
||||
const expectedEntries = createSuccessfulRemoteDataObject(paginatedData.payload);
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
} else if (param === 'sourceId') {
|
||||
return observableOf('scopus');
|
||||
} else if (param === 'query') {
|
||||
return observableOf('test');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||
expect(viewableCollection).toBeTruthy();
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 200 response with no results', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([])));
|
||||
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-entry-list"]'));
|
||||
expect(noDataAlert).toBeTruthy();
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 500 error', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createFailedRemoteDataObject$(
|
||||
errorObj.errorMessage,
|
||||
errorObj.statusCode,
|
||||
errorObj.timeCompleted
|
||||
));
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
} else if (param === 'sourceId') {
|
||||
return observableOf('pubmed');
|
||||
} else if (param === 'query') {
|
||||
return observableOf('test');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value.statusCode).toEqual(500);
|
||||
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-error-500"]'));
|
||||
expect(noDataAlert).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
|
@@ -2983,6 +2983,22 @@
|
||||
|
||||
"process.detail.create" : "Create similar process",
|
||||
|
||||
"process.detail.actions": "Actions",
|
||||
|
||||
"process.detail.delete.button": "Delete process",
|
||||
|
||||
"process.detail.delete.header": "Delete process",
|
||||
|
||||
"process.detail.delete.body": "Are you sure you want to delete the current process?",
|
||||
|
||||
"process.detail.delete.cancel": "Cancel",
|
||||
|
||||
"process.detail.delete.confirm": "Delete process",
|
||||
|
||||
"process.detail.delete.success": "The process was successfully deleted.",
|
||||
|
||||
"process.detail.delete.error": "Something went wrong when deleting the process",
|
||||
|
||||
|
||||
|
||||
"process.overview.table.finish" : "Finish time (UTC)",
|
||||
@@ -3003,6 +3019,25 @@
|
||||
|
||||
"process.overview.new": "New",
|
||||
|
||||
"process.overview.table.actions": "Actions",
|
||||
|
||||
"process.overview.delete": "Delete {{count}} processes",
|
||||
|
||||
"process.overview.delete.clear": "Clear delete selection",
|
||||
|
||||
"process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.",
|
||||
|
||||
"process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?",
|
||||
|
||||
"process.overview.delete.header": "Delete processes",
|
||||
|
||||
"process.bulk.delete.error.head": "Error on deleteing process",
|
||||
|
||||
"process.bulk.delete.error.body": "The process with ID {{processId}} could not be deleted. The remaining processes will continue being deleted. ",
|
||||
|
||||
"process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted",
|
||||
|
||||
|
||||
|
||||
"profile.breadcrumbs": "Update Profile",
|
||||
|
||||
@@ -3583,6 +3618,7 @@
|
||||
|
||||
"search.results.view-result": "View",
|
||||
|
||||
"search.results.response.500": "An error occurred during query execution, please try again later",
|
||||
|
||||
"default.search.results.head": "Search Results",
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
10252
src/assets/i18n/tr.json5
10252
src/assets/i18n/tr.json5
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import { Collection } from '../app/core/shared/collection.model';
|
||||
import { Item } from '../app/core/shared/item.model';
|
||||
import { ITEM } from '../app/core/shared/item.resource-type';
|
||||
import { getItemModuleRoute } from '../app/item-page/item-page-routing-paths';
|
||||
import { HandleService } from '../app/shared/handle.service';
|
||||
|
||||
describe('Theme Models', () => {
|
||||
let theme: Theme;
|
||||
@@ -67,24 +68,40 @@ describe('Theme Models', () => {
|
||||
});
|
||||
|
||||
describe('HandleTheme', () => {
|
||||
let handleService;
|
||||
beforeEach(() => {
|
||||
handleService = new HandleService();
|
||||
});
|
||||
it('should return true when the DSO\'s handle matches the theme\'s handle', () => {
|
||||
theme = new HandleTheme({
|
||||
name: 'matching-handle',
|
||||
handle: '1234/5678',
|
||||
});
|
||||
const dso = Object.assign(new Item(), {
|
||||
}, handleService);
|
||||
const matchingDso = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
uuid: 'item-uuid',
|
||||
handle: '1234/5678',
|
||||
}, handleService);
|
||||
expect(theme.matches('', matchingDso)).toEqual(true);
|
||||
});
|
||||
it('should return false when the DSO\'s handle contains the theme\'s handle as a subpart', () => {
|
||||
theme = new HandleTheme({
|
||||
name: 'matching-handle',
|
||||
handle: '1234/5678',
|
||||
}, handleService);
|
||||
const dso = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
uuid: 'item-uuid',
|
||||
handle: '1234/567891011',
|
||||
});
|
||||
expect(theme.matches('', dso)).toEqual(true);
|
||||
expect(theme.matches('', dso)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when the handles don\'t match', () => {
|
||||
theme = new HandleTheme({
|
||||
name: 'no-matching-handle',
|
||||
handle: '1234/5678',
|
||||
});
|
||||
}, handleService);
|
||||
const dso = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
uuid: 'item-uuid',
|
||||
|
@@ -3,6 +3,9 @@ import { Config } from './config.interface';
|
||||
import { hasValue, hasNoValue, isNotEmpty } from '../app/shared/empty.util';
|
||||
import { DSpaceObject } from '../app/core/shared/dspace-object.model';
|
||||
import { getDSORoute } from '../app/app-routing-paths';
|
||||
import { HandleObject } from '../app/core/shared/handle-object.model';
|
||||
import { Injector } from '@angular/core';
|
||||
import { HandleService } from '../app/shared/handle.service';
|
||||
|
||||
export interface NamedThemeConfig extends Config {
|
||||
name: string;
|
||||
@@ -82,12 +85,20 @@ export class RegExTheme extends Theme {
|
||||
}
|
||||
|
||||
export class HandleTheme extends Theme {
|
||||
constructor(public config: HandleThemeConfig) {
|
||||
|
||||
private normalizedHandle;
|
||||
|
||||
constructor(public config: HandleThemeConfig,
|
||||
protected handleService: HandleService
|
||||
) {
|
||||
super(config);
|
||||
this.normalizedHandle = this.handleService.normalizeHandle(this.config.handle);
|
||||
|
||||
}
|
||||
|
||||
matches(url: string, dso: any): boolean {
|
||||
return hasValue(dso) && hasValue(dso.handle) && dso.handle.includes(this.config.handle);
|
||||
matches<T extends DSpaceObject & HandleObject>(url: string, dso: T): boolean {
|
||||
return hasValue(dso) && hasValue(dso.handle)
|
||||
&& this.handleService.normalizeHandle(dso.handle) === this.normalizedHandle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,11 +112,11 @@ export class UUIDTheme extends Theme {
|
||||
}
|
||||
}
|
||||
|
||||
export const themeFactory = (config: ThemeConfig): Theme => {
|
||||
export const themeFactory = (config: ThemeConfig, injector: Injector): Theme => {
|
||||
if (hasValue((config as RegExThemeConfig).regex)) {
|
||||
return new RegExTheme(config as RegExThemeConfig);
|
||||
} else if (hasValue((config as HandleThemeConfig).handle)) {
|
||||
return new HandleTheme(config as HandleThemeConfig);
|
||||
return new HandleTheme(config as HandleThemeConfig, injector.get(HandleService));
|
||||
} else if (hasValue((config as UUIDThemeConfig).uuid)) {
|
||||
return new UUIDTheme(config as UUIDThemeConfig);
|
||||
} else {
|
||||
|
@@ -1,155 +0,0 @@
|
||||
import { InitService } from '../../app/init.service';
|
||||
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { authReducer } from '../../app/core/auth/auth.reducer';
|
||||
import { storeModuleConfig } from '../../app/app.reducer';
|
||||
import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock';
|
||||
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||
import { AuthService } from '../../app/core/auth/auth.service';
|
||||
import { AuthServiceMock } from '../../app/shared/mocks/auth.service.mock';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterMock } from '../../app/shared/mocks/router.mock';
|
||||
import { MockActivatedRoute } from '../../app/shared/mocks/active-router.mock';
|
||||
import { MenuService } from '../../app/shared/menu/menu.service';
|
||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { AppComponent } from '../../app/app.component';
|
||||
import { RouteService } from '../../app/core/services/route.service';
|
||||
import { getMockLocaleService } from '../../app/app.component.spec';
|
||||
import { MenuServiceStub } from '../../app/shared/testing/menu-service.stub';
|
||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../app/shared/mocks/translate-loader.mock';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../../app/shared/mocks/theme-service.mock';
|
||||
import { BrowserInitService } from './browser-init.service';
|
||||
import { TransferState } from '@angular/platform-browser';
|
||||
|
||||
const initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
loading: false,
|
||||
blocking: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('BrowserInitService', () => {
|
||||
describe('browser-specific initialization steps', () => {
|
||||
let correlationIdServiceSpy;
|
||||
let dspaceTransferStateSpy;
|
||||
let transferStateSpy;
|
||||
let metadataServiceSpy;
|
||||
let breadcrumbsServiceSpy;
|
||||
let klaroServiceSpy;
|
||||
let googleAnalyticsSpy;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [
|
||||
'initCorrelationId',
|
||||
]);
|
||||
dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||
'transfer',
|
||||
]);
|
||||
transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||
'get', 'hasKey'
|
||||
]);
|
||||
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
||||
'listenForRouteChanges',
|
||||
]);
|
||||
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
|
||||
'listenForRouteChange',
|
||||
]);
|
||||
klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [
|
||||
'initialize',
|
||||
]);
|
||||
googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [
|
||||
'addTrackingIdToPage',
|
||||
]);
|
||||
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{ provide: InitService, useClass: BrowserInitService },
|
||||
{ provide: CorrelationIdService, useValue: correlationIdServiceSpy },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: MetadataService, useValue: metadataServiceSpy },
|
||||
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: MenuService, useValue: new MenuServiceStub() },
|
||||
{ provide: KlaroService, useValue: klaroServiceSpy },
|
||||
{ provide: GoogleAnalyticsService, useValue: googleAnalyticsSpy },
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
provideMockStore({ initialState }),
|
||||
AppComponent,
|
||||
RouteService,
|
||||
{ provide: TransferState, useValue: undefined },
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
describe('initGoogleÀnalytics', () => {
|
||||
it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => {
|
||||
// @ts-ignore
|
||||
service.initGoogleAnalytics();
|
||||
expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('initKlaro', () => {
|
||||
const BLOCKING = {
|
||||
t: { core: { auth: { blocking: true } } },
|
||||
f: { core: { auth: { blocking: false } } },
|
||||
};
|
||||
|
||||
it('should not initialize Klaro while auth is blocking', () => {
|
||||
getTestScheduler().run(({ cold, flush}) => {
|
||||
TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) });
|
||||
const service = TestBed.inject(InitService);
|
||||
|
||||
// @ts-ignore
|
||||
service.initKlaro();
|
||||
flush();
|
||||
expect(klaroServiceSpy.initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should only initialize Klaro the first time auth is unblocked', () => {
|
||||
getTestScheduler().run(({ cold, flush}) => {
|
||||
TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) });
|
||||
const service = TestBed.inject(InitService);
|
||||
|
||||
// @ts-ignore
|
||||
service.initKlaro();
|
||||
flush();
|
||||
expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { InitService } from '../../app/init.service';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app/app.reducer';
|
||||
import { TransferState } from '@angular/platform-browser';
|
||||
import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface';
|
||||
@@ -26,9 +26,8 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
||||
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
|
||||
import { coreSelector } from '../../app/core/core.selectors';
|
||||
import { distinctUntilChanged, filter, find, map, take } from 'rxjs/operators';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../app/shared/empty.util';
|
||||
import { isAuthenticationBlocking } from '../../app/core/auth/selectors';
|
||||
|
||||
/**
|
||||
* Performs client-side initialization.
|
||||
@@ -90,6 +89,8 @@ export class BrowserInitService extends InitService {
|
||||
|
||||
this.initKlaro();
|
||||
|
||||
await this.authenticationReady$().toPromise();
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -116,16 +117,11 @@ export class BrowserInitService extends InitService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Klaro
|
||||
* Initialize Klaro (once authentication is resolved)
|
||||
* @protected
|
||||
*/
|
||||
protected initKlaro() {
|
||||
this.store.pipe(
|
||||
select(isAuthenticationBlocking),
|
||||
distinctUntilChanged(),
|
||||
filter((isBlocking: boolean) => isBlocking === false),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.authenticationReady$().subscribe(() => {
|
||||
this.klaroService.initialize();
|
||||
});
|
||||
}
|
||||
|
@@ -66,6 +66,8 @@ export class ServerInitService extends InitService {
|
||||
this.initRouteListeners();
|
||||
this.themeService.listenForThemeChanges(false);
|
||||
|
||||
await this.authenticationReady$().toPromise();
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
@@ -12,6 +12,9 @@ $fa-font-path: "^assets/fonts" !default;
|
||||
/* Images */
|
||||
$image-path: "../assets/images" !default;
|
||||
|
||||
// enable-responsive-font-sizes allows text to scale more naturally across device and viewport sizes
|
||||
$enable-responsive-font-sizes: true;
|
||||
|
||||
/** Bootstrap Variables **/
|
||||
/* Colors */
|
||||
$gray-700: #495057 !default; // Bootstrap $gray-700
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-collection-parent-selector',
|
||||
// styleUrls: ['./create-collection-parent-selector.component.scss'],
|
||||
// templateUrl: './create-collection-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class CreateCollectionParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<button class="btn btn-outline-primary btn-lg btn-block" (click)="selectObject(undefined)">{{'dso-selector.create.community.top-level' | translate}}</button>
|
||||
<h3 class="position-relative py-1 my-3 font-weight-normal">
|
||||
<hr>
|
||||
<div id="create-community-or-separator" class="text-center position-absolute w-100">
|
||||
<span class="px-4 bg-white">or</span>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
#create-community-or-separator {
|
||||
top: 0;
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-community-parent-selector',
|
||||
// styleUrls: ['./create-community-parent-selector.component.scss'],
|
||||
styleUrls: ['../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss'],
|
||||
// templateUrl: './create-community-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html',
|
||||
})
|
||||
export class CreateCommunityParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div [innerHTML]="'dso-selector.create.item.intro' | translate"></div>
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
|
||||
[entityType]="entityType"
|
||||
[types]="selectorTypes"
|
||||
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {
|
||||
CreateItemParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-item-parent-selector',
|
||||
// styleUrls: ['./create-item-parent-selector.component.scss'],
|
||||
// templateUrl: './create-item-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html',
|
||||
})
|
||||
export class CreateItemParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditCollectionSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-collection-selector',
|
||||
// styleUrls: ['./edit-collection-selector.component.scss'],
|
||||
// templateUrl: './edit-collection-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditCollectionSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditCommunitySelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-selector',
|
||||
// styleUrls: ['./edit-community-selector.component.scss'],
|
||||
// templateUrl: './edit-community-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditCommunitySelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditItemSelectorComponent as BaseComponent
|
||||
} from 'src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-selector',
|
||||
// styleUrls: ['./edit-item-selector.component.scss'],
|
||||
// templateUrl: './edit-item-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditItemSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -21,6 +21,24 @@ import {
|
||||
} from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component';
|
||||
import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
|
||||
import { ItemSharedModule } from '../../app/item-page/item-shared.module';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
|
||||
/**
|
||||
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
||||
@@ -41,6 +59,12 @@ const DECLARATIONS = [
|
||||
HeaderNavbarWrapperComponent,
|
||||
NavbarComponent,
|
||||
FooterComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
Reference in New Issue
Block a user