mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into w2p-93889_Invalidate-EPerson-when-their-group-change
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
|
@@ -179,7 +179,7 @@ If needing to update default configurations values for production, update local
|
||||
|
||||
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
|
||||
|
||||
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
|
||||
The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
|
||||
|
||||
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
|
||||
|
||||
|
@@ -150,6 +150,9 @@ languages:
|
||||
- code: fi
|
||||
label: Suomi
|
||||
active: true
|
||||
- code: sv
|
||||
label: Svenska
|
||||
active: true
|
||||
- code: tr
|
||||
label: Türkçe
|
||||
active: true
|
||||
|
@@ -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
@@ -18,6 +18,8 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||
@@ -50,7 +52,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'bbm',
|
||||
id: BBM_PAGINATION_ID,
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
@@ -7,17 +7,24 @@ 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;
|
||||
|
||||
const status = new AuthStatus();
|
||||
|
||||
class TestAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
hes: HALEndpointService,
|
||||
@@ -34,6 +41,7 @@ describe(`AuthRequestService`, () => {
|
||||
|
||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||
endpointURL = 'https://rest.api/auth';
|
||||
requestID = 'requestID';
|
||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||
value: 'some-token'
|
||||
});
|
||||
@@ -43,13 +51,16 @@ describe(`AuthRequestService`, () => {
|
||||
'getEndpoint': cold('a', { a: endpointURL })
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'send': null
|
||||
'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(() => {
|
||||
@@ -58,6 +69,94 @@ describe(`AuthRequestService`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('REST request methods', () => {
|
||||
let options: HttpOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
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;
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
|
||||
|
||||
/**
|
||||
* True when user has authorization rights for the feature and object provided
|
||||
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
||||
* Redirect the user to the unauthorized page when they are not authorized for the given feature
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||
|
@@ -38,7 +38,7 @@ export class ProcessDataService extends DataService<Process> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for a process his files
|
||||
* Get the endpoint for the files of the process
|
||||
* @param processId The ID of the process
|
||||
*/
|
||||
getFilesEndpoint(processId: string): Observable<string> {
|
||||
|
@@ -55,7 +55,7 @@ export class EndUserAgreementService {
|
||||
|
||||
/**
|
||||
* Set the current user's accepted agreement status
|
||||
* When a user is authenticated, set his/her metadata to the provided value
|
||||
* When a user is authenticated, set their metadata to the provided value
|
||||
* When no user is authenticated, set the cookie to the provided value
|
||||
* @param accepted
|
||||
*/
|
||||
|
@@ -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;
|
||||
|
||||
/**
|
||||
|
@@ -39,7 +39,7 @@ export class MetadataValue implements MetadataValueInterface {
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The place of this MetadataValue within his list of metadata
|
||||
* The place of this MetadataValue within its list of metadata
|
||||
* This is used to render metadata in a specific custom order
|
||||
*/
|
||||
@autoserialize
|
||||
@@ -105,7 +105,7 @@ export class MetadatumViewModel {
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The place of this MetadataValue within his list of metadata
|
||||
* The place of this MetadataValue within its list of metadata
|
||||
* This is used to render metadata in a specific custom order
|
||||
*/
|
||||
place: number;
|
||||
|
@@ -16,7 +16,7 @@ export class HealthStatusComponent {
|
||||
@Input() status: HealthStatus;
|
||||
|
||||
/**
|
||||
* He
|
||||
* Health Status
|
||||
*/
|
||||
HealthStatus = HealthStatus;
|
||||
|
||||
|
@@ -76,7 +76,7 @@ export class EndUserAgreementComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Cancel the agreement
|
||||
* If the user is logged in, this will log him/her out
|
||||
* If the user is logged in, this will log them out
|
||||
* If the user is not logged in, they will be redirected to the homepage
|
||||
*/
|
||||
cancel() {
|
||||
|
@@ -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,20 +1,22 @@
|
||||
<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-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>
|
||||
@@ -22,23 +24,28 @@
|
||||
</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()">
|
||||
<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>
|
||||
<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)
|
||||
@@ -47,7 +54,46 @@
|
||||
</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>
|
||||
</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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ import { ConfirmationModalComponent } from '../../shared/confirmation-modal/conf
|
||||
templateUrl: './profile-page-researcher-form.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for a user to create/delete or change his researcher profile.
|
||||
* Component for a user to create/delete or change their researcher profile.
|
||||
*/
|
||||
export class ProfilePageResearcherFormComponent implements OnInit {
|
||||
|
||||
|
@@ -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' })),
|
||||
|
@@ -240,10 +240,12 @@ describe('BrowseByComponent', () => {
|
||||
});
|
||||
describe('back', () => {
|
||||
it('should navigate back to the main browse page', () => {
|
||||
const id = 'test-pagination';
|
||||
comp.back();
|
||||
expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {page: 1}, {
|
||||
expect(paginationService.updateRoute).toHaveBeenCalledWith(id, {page: 1}, {
|
||||
value: null,
|
||||
startsWith: null
|
||||
startsWith: null,
|
||||
[id + '.return']: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { fadeIn, fadeInOut } from '../animations/fade';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
@@ -25,7 +25,7 @@ import { hasValue } from '../empty.util';
|
||||
/**
|
||||
* Component to display a browse-by page for any ListableObject
|
||||
*/
|
||||
export class BrowseByComponent implements OnInit {
|
||||
export class BrowseByComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
|
||||
@@ -112,6 +112,16 @@ export class BrowseByComponent implements OnInit {
|
||||
*/
|
||||
shouldDisplayResetButton$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Page number of the previous page
|
||||
*/
|
||||
previousPage$ = new BehaviorSubject<string>('1');
|
||||
|
||||
/**
|
||||
* Subscription that has to be unsubscribed from on destroy
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
public constructor(private injector: Injector,
|
||||
protected paginationService: PaginationService,
|
||||
private routeService: RouteService,
|
||||
@@ -171,9 +181,20 @@ export class BrowseByComponent implements OnInit {
|
||||
this.shouldDisplayResetButton$ = observableCombineLatest([startsWith$, value$]).pipe(
|
||||
map(([startsWith, value]) => hasValue(startsWith) || hasValue(value))
|
||||
);
|
||||
this.sub = this.routeService.getQueryParameterValue(this.paginationConfig.id + '.return').subscribe(this.previousPage$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back to the previous browse by page
|
||||
*/
|
||||
back() {
|
||||
this.paginationService.updateRoute(this.paginationConfig.id, {page: 1}, {value: null, startsWith: null});
|
||||
const page = +this.previousPage$.value > 1 ? +this.previousPage$.value : 1;
|
||||
this.paginationService.updateRoute(this.paginationConfig.id, {page: page}, {[this.paginationConfig.id + '.return']: null, value: null, startsWith: null});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export const klaroConfiguration: any = {
|
||||
/*
|
||||
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
|
||||
the consent notice. We strongly advise against using this under most
|
||||
circumstances, as it keeps the user from customizing his/her consent choices.
|
||||
circumstances, as it keeps the user from customizing their consent choices.
|
||||
*/
|
||||
hideLearnMore: false,
|
||||
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
}
|
@@ -20,6 +20,7 @@ import {
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../../remote-data.utils';
|
||||
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
|
||||
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
||||
@NgModule({
|
||||
@@ -47,6 +48,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
let router;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService;
|
||||
let authorizationDataService;
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
@@ -95,6 +97,9 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||
}
|
||||
);
|
||||
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
||||
declarations: [ExportMetadataSelectorComponent],
|
||||
@@ -102,6 +107,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
{ provide: NgbActiveModal, useValue: modalStub },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
@@ -150,7 +156,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('if collection is selected', () => {
|
||||
describe('if collection is selected and is admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
@@ -159,7 +165,32 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the metadata-export script with option -i uuid', () => {
|
||||
it('should invoke the metadata-export script with option -i uuid and -a option', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
|
||||
Object.assign(new ProcessParameter(), { name: '-a' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
describe('if collection is selected and is not admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
|
||||
];
|
||||
@@ -174,7 +205,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('if community is selected', () => {
|
||||
describe('if community is selected and is an admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
@@ -183,7 +214,32 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the metadata-export script with option -i uuid', () => {
|
||||
it('should invoke the metadata-export script with option -i uuid and -a option if the user is an admin', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
|
||||
Object.assign(new ProcessParameter(), { name: '-a' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
describe('if community is selected and is not an admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCommunity).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
|
||||
];
|
||||
|
@@ -19,6 +19,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||
|
||||
/**
|
||||
* Component to wrap a list of existing dso's inside a modal
|
||||
@@ -36,6 +38,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
|
||||
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected authorizationDataService: AuthorizationDataService,
|
||||
private modalService: NgbModal) {
|
||||
super(activeModal, route);
|
||||
}
|
||||
@@ -82,8 +85,13 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: dso.uuid }),
|
||||
];
|
||||
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, [])
|
||||
.pipe(
|
||||
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
switchMap((isAdmin) => {
|
||||
if (isAdmin) {
|
||||
parameterValues.push(Object.assign(new ProcessParameter(), {name: '-a'}));
|
||||
}
|
||||
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
}),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Process>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="d-flex flex-row">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="getQueryParams()" [queryParamsHandling]="'merge'" class="lead">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="queryParams$ | async" [queryParamsHandling]="'merge'" class="lead">
|
||||
{{object.value}}
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="lead">
|
||||
|
@@ -4,7 +4,9 @@ import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../utils/truncate.pipe';
|
||||
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
|
||||
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
let browseEntryListElementComponent: BrowseEntryListElementComponent;
|
||||
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
|
||||
|
||||
@@ -13,12 +15,28 @@ const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
|
||||
value: 'De Langhe Kristof'
|
||||
});
|
||||
|
||||
describe('MetadataListElementComponent', () => {
|
||||
let paginationService;
|
||||
let routeService;
|
||||
const pageParam = 'bbm.page';
|
||||
|
||||
function init() {
|
||||
paginationService = jasmine.createSpyObj('paginationService', {
|
||||
getPageParam: pageParam
|
||||
});
|
||||
|
||||
routeService = jasmine.createSpyObj('routeService', {
|
||||
getQueryParameterValue: observableOf('1')
|
||||
});
|
||||
}
|
||||
describe('BrowseEntryListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseEntryListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: { mockValue } }
|
||||
{ provide: 'objectElementProvider', useValue: { mockValue } },
|
||||
{provide: PaginationService, useValue: paginationService},
|
||||
{provide: RouteService, useValue: routeService},
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,9 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { Params } from '@angular/router';
|
||||
import { BBM_PAGINATION_ID } from '../../../browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { RouteService } from 'src/app/core/services/route.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-entry-list-element',
|
||||
@@ -15,16 +21,35 @@ import { listableObjectComponent } from '../../object-collection/shared/listable
|
||||
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
|
||||
*/
|
||||
@listableObjectComponent(BrowseEntry, ViewMode.ListElement)
|
||||
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {
|
||||
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> implements OnInit {
|
||||
/**
|
||||
* Emits the query parameters for the link of this browse entry list element
|
||||
*/
|
||||
queryParams$: Observable<Params>;
|
||||
|
||||
constructor(private paginationService: PaginationService, private routeService: RouteService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.queryParams$ = this.getQueryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params to access the item page of this browse entry.
|
||||
*/
|
||||
public getQueryParams(): {[param: string]: any} {
|
||||
private getQueryParams(): Observable<Params> {
|
||||
const pageParamName = this.paginationService.getPageParam(BBM_PAGINATION_ID);
|
||||
return this.routeService.getQueryParameterValue(pageParamName).pipe(
|
||||
map((currentPage) => {
|
||||
return {
|
||||
value: this.object.value,
|
||||
authority: !!this.object.authority ? this.object.authority : undefined,
|
||||
startsWith: undefined,
|
||||
[pageParamName]: null,
|
||||
[BBM_PAGINATION_ID + '.return']: currentPage
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import { metadataRepresentationComponent } from '../../../metadata-representatio
|
||||
/**
|
||||
* A component for displaying MetadataRepresentation objects in the form of items
|
||||
* It will send the MetadataRepresentation object along with ElementViewMode.SetElement to the ItemTypeSwitcherComponent,
|
||||
* which will in his turn decide how to render the item as metadata.
|
||||
* which will in its turn decide how to render the item as metadata.
|
||||
*/
|
||||
export class ItemMetadataListElementComponent extends MetadataRepresentationListElementComponent {
|
||||
/**
|
||||
|
@@ -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
8078
src/assets/i18n/sv.json5
Normal file
8078
src/assets/i18n/sv.json5
Normal file
File diff suppressed because it is too large
Load Diff
@@ -193,6 +193,7 @@ export class DefaultAppConfig implements AppConfig {
|
||||
{ code: 'pt-PT', label: 'Português', active: true },
|
||||
{ code: 'pt-BR', label: 'Português do Brasil', active: true },
|
||||
{ code: 'fi', label: 'Suomi', active: true },
|
||||
{ code: 'sv', label: 'Svenska', active: true },
|
||||
{ code: 'tr', label: 'Türkçe', active: true },
|
||||
{ code: 'bn', label: 'বাংলা', active: true }
|
||||
];
|
||||
|
@@ -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);
|
||||
});
|
||||
expect(theme.matches('', dso)).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(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
|
||||
|
5
src/styles/_vendor.scss
Normal file
5
src/styles/_vendor.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
// node_modules imports meant for all the themes
|
||||
|
||||
@import '~node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '~node_modules/nouislider/distribute/nouislider.min';
|
||||
@import '~node_modules/ngx-ui-switch/ui-switch.component.scss';
|
@@ -1,8 +1,6 @@
|
||||
@import './helpers/font_awesome_imports.scss';
|
||||
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '../../node_modules/nouislider/distribute/nouislider.min';
|
||||
@import './_vendor.scss';
|
||||
@import './_custom_variables.scss';
|
||||
@import './bootstrap_variables_mapping.scss';
|
||||
@import './_truncatable-part.component.scss';
|
||||
@import './_global-styles.scss';
|
||||
@import '../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
||||
|
@@ -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({
|
||||
|
@@ -4,11 +4,9 @@
|
||||
@import '../../../styles/_variables.scss';
|
||||
@import '../../../styles/_mixins.scss';
|
||||
@import '../../../styles/helpers/font_awesome_imports.scss';
|
||||
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
|
||||
@import '../../../styles/_vendor.scss';
|
||||
@import '../../../styles/_custom_variables.scss';
|
||||
@import './_theme_css_variable_overrides.scss';
|
||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||
@import '../../../styles/_truncatable-part.component.scss';
|
||||
@import './_global-styles.scss';
|
||||
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
||||
|
@@ -4,11 +4,9 @@
|
||||
@import '../../../styles/_variables.scss';
|
||||
@import '../../../styles/_mixins.scss';
|
||||
@import '../../../styles/helpers/font_awesome_imports.scss';
|
||||
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
|
||||
@import '../../../styles/_vendor.scss';
|
||||
@import '../../../styles/_custom_variables.scss';
|
||||
@import './_theme_css_variable_overrides.scss';
|
||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||
@import '../../../styles/_truncatable-part.component.scss';
|
||||
@import './_global-styles.scss';
|
||||
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
||||
|
Reference in New Issue
Block a user