mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 15:03:07 +00:00
Merge branch 'main' into making_user-menu-component_themeable
This commit is contained in:
@@ -16,23 +16,23 @@
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()"
|
||||
<button (click)="onCancel()" type="button"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div between class="btn-group ml-1">
|
||||
<button *ngIf="!isImpersonated" class="btn btn-primary" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
||||
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-primary" (click)="stopImpersonating()">
|
||||
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<button after class="btn btn-danger delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
||||
<button after class="btn btn-danger delete-button" type="button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
</ds-form>
|
||||
|
@@ -36,12 +36,12 @@
|
||||
[displayCancel]="false"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()"
|
||||
<button (click)="onCancel()" type="button"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div after *ngIf="groupBeingEdited != null" class="btn-group">
|
||||
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||
(click)="delete()">
|
||||
(click)="delete()" type="button">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -37,7 +37,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
|
@@ -8,9 +8,9 @@ import {
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { switchMap, take, tap } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
|
||||
@Component({
|
||||
@@ -147,30 +147,48 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit(): void {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema: MetadataSchema) => {
|
||||
const values = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value
|
||||
};
|
||||
if (schema == null) {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
||||
this.submitForm.emit(newSchema);
|
||||
this.registryService
|
||||
.getActiveMetadataSchema()
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((schema: MetadataSchema) => {
|
||||
const metadataValues = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value,
|
||||
};
|
||||
|
||||
let createOrUpdate$: Observable<MetadataSchema>;
|
||||
|
||||
if (schema == null) {
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
Object.assign(new MetadataSchema(), metadataValues)
|
||||
);
|
||||
} else {
|
||||
const updatedSchema = Object.assign(
|
||||
new MetadataSchema(),
|
||||
schema,
|
||||
{
|
||||
namespace: metadataValues.namespace,
|
||||
}
|
||||
);
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
updatedSchema
|
||||
);
|
||||
}
|
||||
|
||||
return createOrUpdate$;
|
||||
}),
|
||||
tap(() => {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
})
|
||||
)
|
||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedOrCreatedSchema);
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
id: schema.id,
|
||||
prefix: schema.prefix,
|
||||
namespace: values.namespace,
|
||||
})).subscribe((updatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedSchema);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -41,7 +41,7 @@
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@@ -12,8 +12,7 @@ import { Router } from '@angular/router';
|
||||
* Represents a non-expandable section in the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-admin-sidebar-section]',
|
||||
selector: 'ds-admin-sidebar-section',
|
||||
templateUrl: './admin-sidebar-section.component.html',
|
||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||
|
||||
|
@@ -26,10 +26,10 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<li *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-nav">
|
||||
|
@@ -15,8 +15,7 @@ import { Router } from '@angular/router';
|
||||
* Represents a expandable section in the sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
||||
selector: 'ds-expandable-admin-sidebar-section',
|
||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||
animations: [rotate, slide, bgColor]
|
||||
|
@@ -161,7 +161,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
if (typeof params.startsWith === 'string'){
|
||||
if (params.startsWith === undefined || params.startsWith === '') {
|
||||
this.startsWith = undefined;
|
||||
}
|
||||
|
||||
if (typeof params.startsWith === 'string'){
|
||||
this.startsWith = params.startsWith.trim();
|
||||
}
|
||||
|
||||
|
@@ -98,9 +98,8 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
||||
// retrieve all entity types to populate the dropdowns selection
|
||||
entities$.subscribe((entityTypes: ItemType[]) => {
|
||||
|
||||
entityTypes
|
||||
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
|
||||
.forEach((type: ItemType, index: number) => {
|
||||
entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
|
||||
entityTypes.forEach((type: ItemType, index: number) => {
|
||||
this.entityTypeSelection.add({
|
||||
disabled: false,
|
||||
label: type.label,
|
||||
@@ -112,7 +111,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
||||
}
|
||||
});
|
||||
|
||||
this.formModel = [...collectionFormModels, this.entityTypeSelection];
|
||||
this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
|
||||
|
||||
super.ngOnInit();
|
||||
this.chd.detectChanges();
|
||||
|
@@ -34,9 +34,6 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
|
@@ -8,7 +8,7 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
||||
<h1>{{ 'communityList.title' | translate }}</h1>
|
||||
<ds-themed-community-list></ds-themed-community-list>
|
||||
</div>
|
||||
|
@@ -25,7 +25,7 @@ import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||
|
||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||
// Helper method to combine and flatten an array of observables of flatNode arrays
|
||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||
observableCombineLatest([...obsList]).pipe(
|
||||
map((matrix: any[][]) => [].concat(...matrix)),
|
||||
@@ -199,7 +199,7 @@ export class CommunityListService {
|
||||
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
|
||||
* followed by flatNodes of its possible subcommunities and collection
|
||||
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
|
||||
* Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections.
|
||||
* Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections.
|
||||
* @param community Community being transformed
|
||||
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
|
||||
* @param parent Flatnode of the parent community
|
||||
@@ -275,7 +275,7 @@ export class CommunityListService {
|
||||
|
||||
/**
|
||||
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
|
||||
* Returns an observable that combines the result.payload.totalElements fo the observables that the
|
||||
* Returns an observable that combines the result.payload.totalElements of the observables that the
|
||||
* respective services return when queried
|
||||
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
|
||||
*/
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackBy">
|
||||
<!-- This is the tree node template for show more node -->
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
||||
class="example-tree-node show-more-node">
|
||||
@@ -34,13 +34,13 @@
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="d-flex flex-row">
|
||||
<h5 class="align-middle pt-2">
|
||||
<span class="align-middle pt-2 lead">
|
||||
<a [routerLink]="node.route" class="lead">
|
||||
{{ dsoNameService.getName(node.payload) }}
|
||||
</a>
|
||||
<span class="pr-2"> </span>
|
||||
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
|
||||
</h5>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ds-truncatable [id]="node.id">
|
||||
|
@@ -28,10 +28,9 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node: FlatNode) => node.level, (node: FlatNode) => true
|
||||
);
|
||||
|
||||
dataSource: CommunityListDatasource;
|
||||
|
||||
paginationConfig: FindListOptions;
|
||||
trackBy = (index, node: FlatNode) => node.id;
|
||||
|
||||
constructor(
|
||||
protected communityListService: CommunityListService,
|
||||
@@ -58,18 +57,28 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
|
||||
}
|
||||
|
||||
// whether or not this node has children (subcommunities or collections)
|
||||
/**
|
||||
* Whether this node has children (subcommunities or collections)
|
||||
* @param _
|
||||
* @param node
|
||||
*/
|
||||
hasChild(_: number, node: FlatNode) {
|
||||
return node.isExpandable$;
|
||||
}
|
||||
|
||||
// whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections
|
||||
/**
|
||||
* Whether this is a show more node that contains no data, but indicates that there is
|
||||
* one or more community or collection.
|
||||
* @param _
|
||||
* @param node
|
||||
*/
|
||||
isShowMore(_: number, node: FlatNode) {
|
||||
return node.isShowMoreNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded
|
||||
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree
|
||||
* so this node is expanded
|
||||
* @param node Node we want to expand
|
||||
*/
|
||||
toggleExpanded(node: FlatNode) {
|
||||
@@ -92,9 +101,12 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
|
||||
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage
|
||||
* > Reloads tree with new page added to corresponding top community lis, sub community list or collection list
|
||||
* @param node The show more node indicating whether it's an increase in top communities, sub communities or collections
|
||||
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity
|
||||
* currentPage
|
||||
* > Reloads tree with new page added to corresponding top community lis, sub community list or
|
||||
* collection list
|
||||
* @param node The show more node indicating whether it's an increase in top communities, sub communities
|
||||
* or collections
|
||||
*/
|
||||
getNextPage(node: FlatNode): void {
|
||||
this.loadingNode = node;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The show more links in the community tree are also represented by a flatNode so we know where in
|
||||
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
|
||||
* the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link)
|
||||
*/
|
||||
export class ShowMoreFlatNode {
|
||||
}
|
||||
|
@@ -21,9 +21,6 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
||||
|
@@ -152,12 +152,12 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
let authMethodModel: AuthMethod;
|
||||
if (splittedRealm.length === 1) {
|
||||
authMethodModel = new AuthMethod(methodName);
|
||||
authMethodModel = new AuthMethod(methodName, Number(j));
|
||||
authMethodModels.push(authMethodModel);
|
||||
} else if (splittedRealm.length > 1) {
|
||||
let location = splittedRealm[1];
|
||||
location = this.parseLocation(location);
|
||||
authMethodModel = new AuthMethod(methodName, location);
|
||||
authMethodModel = new AuthMethod(methodName, Number(j), location);
|
||||
authMethodModels.push(authMethodModel);
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
// make sure the email + password login component gets rendered first
|
||||
authMethodModels = this.sortAuthMethods(authMethodModels);
|
||||
} else {
|
||||
authMethodModels.push(new AuthMethod(AuthMethodType.Password));
|
||||
authMethodModels.push(new AuthMethod(AuthMethodType.Password, 0));
|
||||
}
|
||||
|
||||
return authMethodModels;
|
||||
|
@@ -598,9 +598,9 @@ describe('authReducer', () => {
|
||||
authMethods: [],
|
||||
idle: false
|
||||
};
|
||||
const authMethods = [
|
||||
new AuthMethod(AuthMethodType.Password),
|
||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||
const authMethods: AuthMethod[] = [
|
||||
new AuthMethod(AuthMethodType.Password, 0),
|
||||
new AuthMethod(AuthMethodType.Shibboleth, 1, 'location'),
|
||||
];
|
||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
||||
const newState = authReducer(initialState, action);
|
||||
@@ -632,7 +632,7 @@ describe('authReducer', () => {
|
||||
loaded: false,
|
||||
blocking: false,
|
||||
loading: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password, 0)],
|
||||
idle: false
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
|
@@ -236,7 +236,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
blocking: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password, 0)]
|
||||
});
|
||||
|
||||
case AuthActionTypes.SET_REDIRECT_URL:
|
||||
|
@@ -2,11 +2,12 @@ import { AuthMethodType } from './auth.method-type';
|
||||
|
||||
export class AuthMethod {
|
||||
authMethodType: AuthMethodType;
|
||||
position: number;
|
||||
location?: string;
|
||||
|
||||
// isStandalonePage? = true;
|
||||
constructor(authMethodName: string, position: number, location?: string) {
|
||||
this.position = position;
|
||||
|
||||
constructor(authMethodName: string, location?: string) {
|
||||
switch (authMethodName) {
|
||||
case 'ip': {
|
||||
this.authMethodType = AuthMethodType.Ip;
|
||||
|
@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||
import { RouteEffects } from './services/route.effects';
|
||||
import { RouterEffects } from './router/router.effects';
|
||||
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||
|
||||
export const coreEffects = [
|
||||
RequestEffects,
|
||||
@@ -18,4 +19,5 @@ export const coreEffects = [
|
||||
ObjectUpdatesEffects,
|
||||
RouteEffects,
|
||||
RouterEffects,
|
||||
MenuEffects,
|
||||
];
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { EMPTY, of as observableOf } from 'rxjs';
|
||||
import { EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
||||
@@ -638,4 +638,87 @@ describe('RequestService', () => {
|
||||
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('setStaleByHref', () => {
|
||||
const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f';
|
||||
const href = 'https://rest.api/some/object';
|
||||
const freshRE: any = {
|
||||
request: { uuid, href },
|
||||
state: RequestEntryState.Success
|
||||
};
|
||||
const staleRE: any = {
|
||||
request: { uuid, href },
|
||||
state: RequestEntryState.SuccessStale
|
||||
};
|
||||
|
||||
it(`should call getByHref to retrieve the RequestEntry matching the href`, () => {
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||
service.setStaleByHref(href);
|
||||
expect(service.getByHref).toHaveBeenCalledWith(href);
|
||||
});
|
||||
|
||||
it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => {
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||
spyOn(store, 'dispatch');
|
||||
service.setStaleByHref(href).subscribe(() => {
|
||||
const requestStaleAction = new RequestStaleAction(uuid);
|
||||
requestStaleAction.lastUpdated = jasmine.any(Number) as any;
|
||||
expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should emit true when the request in the store is stale`, () => {
|
||||
spyOn(service, 'getByHref').and.returnValue(cold('a-b', {
|
||||
a: freshRE,
|
||||
b: staleRE
|
||||
}));
|
||||
const result$ = service.setStaleByHref(href);
|
||||
expect(result$).toBeObservable(cold('--(c|)', { c: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setStaleByHrefSubstring', () => {
|
||||
let dispatchSpy: jasmine.Spy;
|
||||
let getByUUIDSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = spyOn(store, 'dispatch');
|
||||
getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough();
|
||||
});
|
||||
|
||||
describe('with an empty/no matching requests in the state', () => {
|
||||
it('should return true', () => {
|
||||
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
|
||||
expect(done$).toBeObservable(cold('(a|)', { a: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a matching request in the state', () => {
|
||||
beforeEach(() => {
|
||||
const state = Object.assign({}, initialState, {
|
||||
core: Object.assign({}, initialState.core, {
|
||||
'index': {
|
||||
'get-request/href-to-uuid': {
|
||||
'https://rest.api/endpoint/selfLink': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
mockStore.setState(state);
|
||||
});
|
||||
|
||||
it('should return an Observable that emits true as soon as the request is stale', () => {
|
||||
dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale
|
||||
getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache
|
||||
a: { state: RequestEntryState.ResponsePending },
|
||||
b: { state: RequestEntryState.Success },
|
||||
c: { state: RequestEntryState.SuccessStale },
|
||||
d: { state: RequestEntryState.Error },
|
||||
}));
|
||||
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
|
||||
expect(done$).toBeObservable(cold('-----(a|)', { a: true }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take, tap } from 'rxjs/operators';
|
||||
import { Observable, from as observableFrom } from 'rxjs';
|
||||
import { filter, find, map, mergeMap, switchMap, take, tap, toArray } from 'rxjs/operators';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
|
||||
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
RequestExecuteAction,
|
||||
RequestStaleAction
|
||||
} from './request.actions';
|
||||
import { GetRequest} from './request.models';
|
||||
import { GetRequest } from './request.models';
|
||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
@@ -300,22 +300,42 @@ export class RequestService {
|
||||
* Set all requests that match (part of) the href to stale
|
||||
*
|
||||
* @param href A substring of the request(s) href
|
||||
* @return Returns an observable emitting whether or not the cache is removed
|
||||
* @return Returns an observable emitting when those requests are all stale
|
||||
*/
|
||||
setStaleByHrefSubstring(href: string): Observable<boolean> {
|
||||
this.store.pipe(
|
||||
const requestUUIDs$ = this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
take(1)
|
||||
).subscribe((uuids: string[]) => {
|
||||
);
|
||||
requestUUIDs$.subscribe((uuids: string[]) => {
|
||||
for (const uuid of uuids) {
|
||||
this.store.dispatch(new RequestStaleAction(uuid));
|
||||
}
|
||||
});
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||
|
||||
return this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
map((uuids) => isEmpty(uuids))
|
||||
// emit true after all requests are stale
|
||||
return requestUUIDs$.pipe(
|
||||
switchMap((uuids: string[]) => {
|
||||
if (isEmpty(uuids)) {
|
||||
// if there were no matching requests, emit true immediately
|
||||
return [true];
|
||||
} else {
|
||||
// otherwise emit all request uuids in order
|
||||
return observableFrom(uuids).pipe(
|
||||
// retrieve the RequestEntry for each uuid
|
||||
mergeMap((uuid: string) => this.getByUUID(uuid)),
|
||||
// check whether it is undefined or stale
|
||||
map((request: RequestEntry) => hasNoValue(request) || isStale(request.state)),
|
||||
// if it is, complete
|
||||
find((stale: boolean) => stale === true),
|
||||
// after all observables above are completed, emit them as a single array
|
||||
toArray(),
|
||||
// when the array comes in, emit true
|
||||
map(() => true)
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,7 +351,29 @@ export class RequestService {
|
||||
map((request: RequestEntry) => isStale(request.state)),
|
||||
filter((stale: boolean) => stale),
|
||||
take(1),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a request as stale
|
||||
* @param href the href of the request
|
||||
* @return an Observable that will emit true once the Request becomes stale
|
||||
*/
|
||||
setStaleByHref(href: string): Observable<boolean> {
|
||||
const requestEntry$ = this.getByHref(href);
|
||||
|
||||
requestEntry$.pipe(
|
||||
map((re: RequestEntry) => re.request.uuid),
|
||||
take(1),
|
||||
).subscribe((uuid: string) => {
|
||||
this.store.dispatch(new RequestStaleAction(uuid));
|
||||
});
|
||||
|
||||
return requestEntry$.pipe(
|
||||
map((request: RequestEntry) => isStale(request.state)),
|
||||
filter((stale: boolean) => stale),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,10 +386,10 @@ export class RequestService {
|
||||
// if it's not a GET request
|
||||
if (request.method !== RestRequestMethod.GET) {
|
||||
return true;
|
||||
// if it is a GET request, check it isn't pending
|
||||
// if it is a GET request, check it isn't pending
|
||||
} else if (this.isPending(request)) {
|
||||
return false;
|
||||
// if it is pending, check if we're allowed to use a cached version
|
||||
// if it is pending, check if we're allowed to use a cached version
|
||||
} else if (!useCachedVersionIfAvailable) {
|
||||
return true;
|
||||
} else {
|
||||
|
@@ -1,16 +1,18 @@
|
||||
import { RootDataService } from './root-data.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject$,
|
||||
createFailedRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { Root } from './root.model';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
describe('RootDataService', () => {
|
||||
let service: RootDataService;
|
||||
let halService: HALEndpointService;
|
||||
let restService;
|
||||
let requestService;
|
||||
let rootEndpoint;
|
||||
let findByHrefSpy;
|
||||
|
||||
@@ -19,10 +21,10 @@ describe('RootDataService', () => {
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getRootHref: rootEndpoint,
|
||||
});
|
||||
restService = jasmine.createSpyObj('halService', {
|
||||
get: jasmine.createSpy('get'),
|
||||
});
|
||||
service = new RootDataService(null, null, null, halService, restService);
|
||||
requestService = jasmine.createSpyObj('requestService', [
|
||||
'setStaleByHref',
|
||||
]);
|
||||
service = new RootDataService(requestService, null, null, halService);
|
||||
|
||||
findByHrefSpy = spyOn(service as any, 'findByHref');
|
||||
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||
@@ -47,12 +49,8 @@ describe('RootDataService', () => {
|
||||
let result$: Observable<boolean>;
|
||||
|
||||
it('should return observable of true when root endpoint is available', () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
} as RawRestResponse;
|
||||
spyOn(service, 'findRoot').and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
|
||||
|
||||
restService.get.and.returnValue(of(mockResponse));
|
||||
result$ = service.checkServerAvailability();
|
||||
|
||||
expect(result$).toBeObservable(cold('(a|)', {
|
||||
@@ -61,12 +59,8 @@ describe('RootDataService', () => {
|
||||
});
|
||||
|
||||
it('should return observable of false when root endpoint is not available', () => {
|
||||
const mockResponse = {
|
||||
statusCode: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
} as RawRestResponse;
|
||||
spyOn(service, 'findRoot').and.returnValue(createFailedRemoteDataObject$<Root>('500'));
|
||||
|
||||
restService.get.and.returnValue(of(mockResponse));
|
||||
result$ = service.checkServerAvailability();
|
||||
|
||||
expect(result$).toBeObservable(cold('(a|)', {
|
||||
@@ -75,4 +69,12 @@ describe('RootDataService', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe(`invalidateRootCache`, () => {
|
||||
it(`should set the cached root request to stale`, () => {
|
||||
service.invalidateRootCache();
|
||||
expect(halService.getRootHref).toHaveBeenCalled();
|
||||
expect(requestService.setStaleByHref).toHaveBeenCalledWith(rootEndpoint);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -7,12 +7,11 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { BaseDataService } from './base/base-data.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { dataService } from './base/data-service.decorator';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
|
||||
/**
|
||||
* A service to retrieve the {@link Root} object from the REST API.
|
||||
@@ -25,7 +24,6 @@ export class RootDataService extends BaseDataService<Root> {
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected restService: DspaceRestService,
|
||||
) {
|
||||
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
|
||||
}
|
||||
@@ -34,12 +32,13 @@ export class RootDataService extends BaseDataService<Root> {
|
||||
* Check if root endpoint is available
|
||||
*/
|
||||
checkServerAvailability(): Observable<boolean> {
|
||||
return this.restService.get(this.halService.getRootHref()).pipe(
|
||||
return this.findRoot().pipe(
|
||||
catchError((err ) => {
|
||||
console.error(err);
|
||||
return observableOf(false);
|
||||
}),
|
||||
map((res: RawRestResponse) => res.statusCode === 200)
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rootRd: RemoteData<Root>) => rootRd.statusCode === 200)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,6 +59,6 @@ export class RootDataService extends BaseDataService<Root> {
|
||||
* Set to sale the root endpoint cache hit
|
||||
*/
|
||||
invalidateRootCache() {
|
||||
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
|
||||
this.requestService.setStaleByHref(this.halService.getRootHref());
|
||||
}
|
||||
}
|
||||
|
@@ -1,68 +1,86 @@
|
||||
import { ServerCheckGuard } from './server-check.guard';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, NavigationStart, UrlTree, NavigationEnd, RouterEvent } from '@angular/router';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||
import { of, ReplaySubject } from 'rxjs';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
|
||||
describe('ServerCheckGuard', () => {
|
||||
let guard: ServerCheckGuard;
|
||||
let router: SpyObj<Router>;
|
||||
let router: Router;
|
||||
const eventSubject = new ReplaySubject<RouterEvent>(1);
|
||||
let rootDataServiceStub: SpyObj<RootDataService>;
|
||||
|
||||
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
||||
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
|
||||
});
|
||||
router = jasmine.createSpyObj('Router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
let testScheduler: TestScheduler;
|
||||
let redirectUrlTree: UrlTree;
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
||||
invalidateRootCache: jasmine.createSpy('invalidateRootCache'),
|
||||
findRoot: jasmine.createSpy('findRoot')
|
||||
});
|
||||
redirectUrlTree = new UrlTree();
|
||||
router = {
|
||||
events: eventSubject.asObservable(),
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
|
||||
} as any;
|
||||
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
router.navigateByUrl.calls.reset();
|
||||
rootDataServiceStub.invalidateRootCache.calls.reset();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(guard).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when root endpoint has succeeded', () => {
|
||||
describe('when root endpoint request has succeeded', () => {
|
||||
beforeEach(() => {
|
||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
||||
});
|
||||
|
||||
it('should not redirect to error page', () => {
|
||||
guard.canActivateChild({} as any, {} as any).pipe(
|
||||
take(1)
|
||||
).subscribe((canActivate: boolean) => {
|
||||
expect(canActivate).toEqual(true);
|
||||
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
it('should return true', () => {
|
||||
testScheduler.run(({ expectObservable }) => {
|
||||
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||
expectObservable(result$).toBe('(a|)', { a: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when root endpoint has not succeeded', () => {
|
||||
describe('when root endpoint request has not succeeded', () => {
|
||||
beforeEach(() => {
|
||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
||||
});
|
||||
|
||||
it('should redirect to error page', () => {
|
||||
guard.canActivateChild({} as any, {} as any).pipe(
|
||||
take(1)
|
||||
).subscribe((canActivate: boolean) => {
|
||||
expect(canActivate).toEqual(false);
|
||||
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
|
||||
it('should return a UrlTree with the route to the 500 error page', () => {
|
||||
testScheduler.run(({ expectObservable }) => {
|
||||
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
|
||||
});
|
||||
expect(router.parseUrl).toHaveBeenCalledWith('/500');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`listenForRouteChanges`, () => {
|
||||
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => {
|
||||
testScheduler.run(() => {
|
||||
guard.listenForRouteChanges();
|
||||
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should invalidate the root cache on every NavigationStart event`, () => {
|
||||
testScheduler.run(() => {
|
||||
guard.listenForRouteChanges();
|
||||
eventSubject.next(new NavigationStart(1,''));
|
||||
eventSubject.next(new NavigationEnd(1,'', ''));
|
||||
eventSubject.next(new NavigationStart(2,''));
|
||||
eventSubject.next(new NavigationEnd(2,'', ''));
|
||||
eventSubject.next(new NavigationStart(3,''));
|
||||
});
|
||||
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateChild,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
UrlTree,
|
||||
NavigationStart
|
||||
} from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { take, tap } from 'rxjs/operators';
|
||||
import { take, map, filter } from 'rxjs/operators';
|
||||
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||
@@ -23,17 +30,38 @@ export class ServerCheckGuard implements CanActivateChild {
|
||||
*/
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean> {
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean | UrlTree> {
|
||||
|
||||
return this.rootDataService.checkServerAvailability().pipe(
|
||||
take(1),
|
||||
tap((isAvailable: boolean) => {
|
||||
map((isAvailable: boolean) => {
|
||||
if (!isAvailable) {
|
||||
this.rootDataService.invalidateRootCache();
|
||||
this.router.navigateByUrl(getPageInternalServerErrorRoute());
|
||||
return this.router.parseUrl(getPageInternalServerErrorRoute());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to all router events. Every time a new navigation starts, invalidate the cache
|
||||
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
|
||||
* backend is not down. But if the guard is called multiple times during the same routing
|
||||
* operation, the cached version is used.
|
||||
*/
|
||||
listenForRouteChanges(): void {
|
||||
// we'll always be too late for the first NavigationStart event with the router subscribe below,
|
||||
// so this statement is for the very first route operation. A `find` without using the cache,
|
||||
// rather than an invalidateRootCache, because invalidating as the app is bootstrapping can
|
||||
// break other features
|
||||
this.rootDataService.findRoot(false);
|
||||
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationStart),
|
||||
).subscribe(() => {
|
||||
this.rootDataService.invalidateRootCache();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -38,8 +38,8 @@ export class BrowserHardRedirectService extends HardRedirectService {
|
||||
/**
|
||||
* Get the origin of the current URL
|
||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
||||
* the origin would be https://demo7.dspace.org
|
||||
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||
* the origin would be https://demo.dspace.org
|
||||
*/
|
||||
getCurrentOrigin(): string {
|
||||
return this.location.origin;
|
||||
|
@@ -25,8 +25,8 @@ export abstract class HardRedirectService {
|
||||
/**
|
||||
* Get the origin of the current URL
|
||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
||||
* the origin would be https://demo7.dspace.org
|
||||
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||
* the origin would be https://demo.dspace.org
|
||||
*/
|
||||
abstract getCurrentOrigin(): string;
|
||||
}
|
||||
|
@@ -69,8 +69,8 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
/**
|
||||
* Get the origin of the current URL
|
||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
||||
* the origin would be https://demo7.dspace.org
|
||||
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||
* the origin would be https://demo.dspace.org
|
||||
*/
|
||||
getCurrentOrigin(): string {
|
||||
return this.req.protocol + '://' + this.req.headers.host;
|
||||
|
@@ -29,6 +29,18 @@ export class WorkspaceitemSectionUploadFileObject {
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The file format information
|
||||
*/
|
||||
format: {
|
||||
shortDescription: string,
|
||||
description: string,
|
||||
mimetype: string,
|
||||
supportLevel: string,
|
||||
internal: boolean,
|
||||
type: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The file url
|
||||
*/
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { DsoEditMetadataForm } from './dso-edit-metadata-form';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { ContextHelpService } from '../../shared/context-help.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
@@ -15,12 +15,23 @@ import { map } from 'rxjs/operators';
|
||||
export class ContextHelpToggleComponent implements OnInit {
|
||||
buttonVisible$: Observable<boolean>;
|
||||
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private contextHelpService: ContextHelpService,
|
||||
) { }
|
||||
protected elRef: ElementRef,
|
||||
protected contextHelpService: ContextHelpService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
|
||||
this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => {
|
||||
if (showContextHelpToggle) {
|
||||
this.elRef.nativeElement.classList.remove('d-none');
|
||||
} else {
|
||||
this.elRef.nativeElement.classList.add('d-none');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onClick() {
|
||||
|
@@ -7,12 +7,12 @@
|
||||
|
||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
||||
<ds-themed-search-navbar></ds-themed-search-navbar>
|
||||
<ds-lang-switch></ds-lang-switch>
|
||||
<ds-themed-lang-switch></ds-themed-lang-switch>
|
||||
<ds-context-help-toggle></ds-context-help-toggle>
|
||||
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||
<div class="pl-2">
|
||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||
<div *ngIf="isXsOrSm$ | async" class="pl-2">
|
||||
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
||||
aria-controls="collapsingNav"
|
||||
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||
|
@@ -20,3 +20,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
gap: calc(var(--bs-spacer) / 3);
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||
|
||||
let comp: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
@@ -26,6 +28,7 @@ describe('HeaderComponent', () => {
|
||||
ReactiveFormsModule],
|
||||
declarations: [HeaderComponent],
|
||||
providers: [
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: MenuService, useValue: menuService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -40,7 +43,7 @@ describe('HeaderComponent', () => {
|
||||
fixture = TestBed.createComponent(HeaderComponent);
|
||||
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the toggle button is clicked', () => {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuID } from '../shared/menu/menu-id.model';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
|
||||
/**
|
||||
* Represents the header with the logo and simple navigation
|
||||
@@ -11,20 +12,25 @@ import { MenuID } from '../shared/menu/menu-id.model';
|
||||
styleUrls: ['header.component.scss'],
|
||||
templateUrl: 'header.component.html',
|
||||
})
|
||||
export class HeaderComponent {
|
||||
export class HeaderComponent implements OnInit {
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
public showAuth = false;
|
||||
public isXsOrSm$: Observable<boolean>;
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(
|
||||
private menuService: MenuService
|
||||
protected menuService: MenuService,
|
||||
protected windowService: HostWindowService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
public toggleNavbar(): void {
|
||||
this.menuService.toggleMenu(this.menuID);
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { HealthComponent } from '../../models/health-component.model';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
|
||||
/**
|
||||
* A component to render a "health component" object.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-alerts',
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<div class="btn-group relationship-action-buttons">
|
||||
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
|
||||
<a *ngIf="bitstreamDownloadUrl != null" [routerLink]="bitstreamDownloadUrl"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
||||
[attr.data-test]="'download-button' | dsBrowserOnly">
|
||||
|
@@ -13,6 +13,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
let comp: ItemEditBitstreamComponent;
|
||||
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
||||
@@ -72,7 +73,10 @@ describe('ItemEditBitstreamComponent', () => {
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
ItemEditBitstreamComponent,
|
||||
VarDirective,
|
||||
|
@@ -5,7 +5,7 @@ import { Item } from '../../../core/shared/item.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-version-history',
|
||||
|
@@ -15,7 +15,7 @@ import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<h2 class="item-page-title-field">
|
||||
<h1 class="item-page-title-field">
|
||||
<div *ngIf="item.firstMetadataValue('dspace.entity.type') as type" class="d-inline">
|
||||
{{ type.toLowerCase() + '.page.titleprefix' | translate }}
|
||||
</div>
|
||||
<span class="dont-break-out">{{ dsoNameService.getName(item) }}</span>
|
||||
</h2>
|
||||
</h1>
|
||||
|
@@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteData
|
||||
getFirstCompletedRemoteData
|
||||
} from '../../../../core/shared/operators';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
@@ -77,24 +76,42 @@ export const relationsToItems = (thisId: string) =>
|
||||
* @param {string} thisId The item's id of which the relations belong to
|
||||
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
|
||||
*/
|
||||
export const paginatedRelationsToItems = (thisId: string) =>
|
||||
(source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
|
||||
export const paginatedRelationsToItems = (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
|
||||
source.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
||||
return observableCombineLatest(
|
||||
relationshipsRD.payload.page.map((rel: Relationship) =>
|
||||
observableCombineLatest([
|
||||
rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()),
|
||||
rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())]
|
||||
rel.leftItem.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Item>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
),
|
||||
rel.rightItem.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Item>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
),
|
||||
]
|
||||
)
|
||||
)).pipe(
|
||||
)
|
||||
).pipe(
|
||||
map((arr) =>
|
||||
arr
|
||||
.map(([leftItem, rightItem]) => {
|
||||
if (leftItem.id === thisId) {
|
||||
arr.map(([leftItem, rightItem]) => {
|
||||
if (hasValue(leftItem) && leftItem.id === thisId) {
|
||||
return rightItem;
|
||||
} else if (rightItem.id === thisId) {
|
||||
} else if (hasValue(rightItem) && rightItem.id === thisId) {
|
||||
return leftItem;
|
||||
}
|
||||
})
|
||||
|
@@ -2,5 +2,6 @@
|
||||
[fixedFilterQuery]="fixedFilter"
|
||||
[configuration]="configuration"
|
||||
[searchEnabled]="searchEnabled"
|
||||
[sideBarWidth]="sideBarWidth">
|
||||
[sideBarWidth]="sideBarWidth"
|
||||
[showCsvExport]="true">
|
||||
</ds-configuration-search-page>
|
||||
|
@@ -23,7 +23,7 @@ import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { VersionHistoryDataService } from '../../core/data/version-history-data.service';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { hasValue, hasValueOperator } from '../../shared/empty.util';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
} from '../../../core/shared/operators';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
|
@@ -14,9 +14,9 @@
|
||||
</a>
|
||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||
<ng-container *ngFor="let subSection of (subSections$ | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -4,7 +4,6 @@ import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { slide } from '../../shared/animations/slide';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,6 @@ import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
styleUrls: ['./expandable-navbar-section.component.scss'],
|
||||
animations: [slide]
|
||||
})
|
||||
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
||||
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
||||
/**
|
||||
* This section resides in the Public Navbar
|
||||
|
@@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
* Themed wrapper for ExpandableNavbarSectionComponent
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-themed-expandable-navbar-section]',
|
||||
selector: 'ds-themed-expandable-navbar-section',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
@@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
* Represents a non-expandable section in the navbar
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-navbar-section]',
|
||||
selector: 'ds-navbar-section',
|
||||
templateUrl: './navbar-section.component.html',
|
||||
styleUrls: ['./navbar-section.component.scss']
|
||||
})
|
||||
|
@@ -8,9 +8,9 @@
|
||||
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
|
||||
<ds-themed-user-menu [inExpandableNavbar]="true"></ds-themed-user-menu>
|
||||
</li>
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<li *ngFor="let section of (sections | async)">
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -147,7 +147,7 @@ describe('ProcessDetailComponent', () => {
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) }
|
||||
useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }), snapshot: { params: { id: 1 } } },
|
||||
},
|
||||
{ provide: ProcessDataService, useValue: processService },
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
@@ -310,10 +310,11 @@ describe('ProcessDetailComponent', () => {
|
||||
});
|
||||
|
||||
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
|
||||
spyOn(component, 'refresh');
|
||||
spyOn(component, 'stopRefreshTimer');
|
||||
spyOn(component, 'refresh').and.callThrough();
|
||||
spyOn(component, 'stopRefreshTimer').and.callThrough();
|
||||
|
||||
process.processStatus = ProcessStatus.COMPLETED;
|
||||
// start off with a running process in order for the refresh counter starts counting up
|
||||
process.processStatus = ProcessStatus.RUNNING;
|
||||
// set findbyId to return a completed process
|
||||
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||
|
||||
@@ -336,6 +337,10 @@ describe('ProcessDetailComponent', () => {
|
||||
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
|
||||
|
||||
// set the process to completed right before the counter checks the process
|
||||
process.processStatus = ProcessStatus.COMPLETED;
|
||||
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||
|
||||
tick(1000); // 1 second
|
||||
|
||||
expect(component.refresh).toHaveBeenCalledTimes(1);
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { Process } from '../processes/process.model';
|
||||
|
@@ -161,7 +161,7 @@ export class ProfilePageComponent implements OnInit {
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.title'),
|
||||
this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.change-failed')
|
||||
this.getPasswordErrorMessage(response)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -199,4 +199,18 @@ export class ProfilePageComponent implements OnInit {
|
||||
return this.isResearcherProfileEnabled$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error message from a password validation request with a specific reason or
|
||||
* a default message without specific reason.
|
||||
* @param response from the validation password patch request.
|
||||
*/
|
||||
getPasswordErrorMessage(response) {
|
||||
if (response.hasFailed && isNotEmpty(response.errorMessage)) {
|
||||
// Response has a specific error message. Show this message in the error notification.
|
||||
return this.translate.instant(response.errorMessage);
|
||||
}
|
||||
// Show default error message notification.
|
||||
return this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.change-failed');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import {isNotEmpty} from '../shared/empty.util';
|
||||
import {BehaviorSubject, combineLatest, Observable, of, switchMap} from 'rxjs';
|
||||
import {map, startWith, take} from 'rxjs/operators';
|
||||
import {CAPTCHA_NAME, GoogleRecaptchaService} from '../core/google-recaptcha/google-recaptcha.service';
|
||||
import {AlertType} from '../shared/alert/aletr-type';
|
||||
import {AlertType} from '../shared/alert/alert-type';
|
||||
import {KlaroService} from '../shared/cookies/klaro.service';
|
||||
import {CookieService} from '../core/services/cookie.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
@@ -13,7 +13,7 @@
|
||||
<ng-content></ng-content>
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<button (click)="submit()"
|
||||
[disabled]="!message || message.length === 0 || !subject || subject.length === 0"
|
||||
[disabled]="!subject || subject.length === 0"
|
||||
class="btn btn-primary"
|
||||
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
||||
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
<div id="search-navbar-container" [title]="'nav.search' | translate" (dsClickOutside)="collapse()">
|
||||
<div class="d-inline-block position-relative">
|
||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
|
||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on" class="d-flex">
|
||||
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
|
||||
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
|
||||
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" [attr.data-test]="'header-search-box' | dsBrowserOnly">
|
||||
class="bg-transparent position-absolute form-control dropdown-menu-right pl-1 pr-4"
|
||||
[class.display]="searchExpanded ? 'inline-block' : 'none'"
|
||||
[tabIndex]="searchExpanded ? 0 : -1"
|
||||
[attr.data-test]="'header-search-box' | dsBrowserOnly">
|
||||
<button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
|
||||
<em class="fas fa-search fa-lg fa-fw"></em>
|
||||
</button>
|
||||
|
@@ -12,6 +12,7 @@ input[type="text"] {
|
||||
cursor: pointer;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
border: 0 !important;
|
||||
|
||||
color: var(--ds-header-icon-color);
|
||||
&:hover, &:focus {
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
import { BulkAccessConfigDataService } from '../../core/config/bulk-access-config-data.service';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { BulkAccessConditionOptions } from '../../core/config/models/bulk-access-condition-options.model';
|
||||
import { AlertType } from '../alert/aletr-type';
|
||||
import { AlertType } from '../alert/alert-type';
|
||||
import {
|
||||
createAccessControlInitialFormState
|
||||
} from './access-control-form-container-intial-state';
|
||||
|
@@ -8,7 +8,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { AlertComponent } from './alert.component';
|
||||
import { createTestComponent } from '../testing/utils.test';
|
||||
import { AlertType } from './aletr-type';
|
||||
import { AlertType } from './alert-type';
|
||||
|
||||
describe('AlertComponent test suite', () => {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { trigger } from '@angular/animations';
|
||||
|
||||
import { AlertType } from './aletr-type';
|
||||
import { AlertType } from './alert-type';
|
||||
import { fadeOutLeave, fadeOutState } from '../animations/fade';
|
||||
|
||||
/**
|
||||
|
@@ -55,7 +55,7 @@ export const slideSidebarPadding = trigger('slideSidebarPadding', [
|
||||
|
||||
export const expandSearchInput = trigger('toggleAnimation', [
|
||||
state('collapsed', style({
|
||||
width: '30px',
|
||||
width: '0',
|
||||
opacity: '0'
|
||||
})),
|
||||
state('expanded', style({
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
||||
(click)="$event.stopPropagation();">
|
||||
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="javascript:void(0);" class="dropdownLogin px-1" [attr.aria-label]="'nav.login' |translate"
|
||||
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
|
||||
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
|
||||
ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
|
||||
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a routerLink="/login" routerLinkActive="active" class="loginLink px-1">
|
||||
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5">
|
||||
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@@ -42,7 +42,7 @@
|
||||
[formModel]="formModel"
|
||||
[displayCancel]="false"
|
||||
(submitForm)="onSubmit()">
|
||||
<button before (click)="back.emit()" class="btn btn-outline-secondary">
|
||||
<button before (click)="back.emit()" class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-arrow-left"></i> {{ type.value + '.edit.return' | translate }}
|
||||
</button>
|
||||
</ds-form>
|
||||
|
@@ -22,7 +22,7 @@ export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics';
|
||||
export const klaroConfiguration: any = {
|
||||
storageName: ANONYMOUS_STORAGE_NAME_KLARO,
|
||||
|
||||
privacyPolicy: '/info/privacy',
|
||||
privacyPolicy: './info/privacy',
|
||||
|
||||
/*
|
||||
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MenuServiceStub } from '../testing/menu-service.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { combineLatest, map, of as observableOf } from 'rxjs';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
@@ -16,10 +16,13 @@ import { Item } from '../../core/shared/item.model';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { MenuID } from '../menu/menu-id.model';
|
||||
import { MenuItemType } from '../menu/menu-item-type.model';
|
||||
import { TextMenuItemModel } from '../menu/menu-item/models/text.model';
|
||||
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
|
||||
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import flatten from 'lodash/flatten';
|
||||
|
||||
describe('DSOEditMenuResolver', () => {
|
||||
|
||||
@@ -37,25 +40,44 @@ describe('DSOEditMenuResolver', () => {
|
||||
let notificationsService;
|
||||
let translate;
|
||||
|
||||
const route = {
|
||||
data: {
|
||||
menu: {
|
||||
'statistics': [{
|
||||
id: 'statistics-dummy-1',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null
|
||||
}]
|
||||
}
|
||||
},
|
||||
params: {id: 'test-uuid'},
|
||||
const dsoRoute = (dso: DSpaceObject) => {
|
||||
return {
|
||||
data: {
|
||||
menu: {
|
||||
'statistics': [{
|
||||
id: 'statistics-dummy-1',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null
|
||||
}]
|
||||
}
|
||||
},
|
||||
params: {id: dso.uuid},
|
||||
};
|
||||
};
|
||||
|
||||
const state = {
|
||||
url: 'test-url'
|
||||
};
|
||||
|
||||
const testObject = Object.assign(new Item(), {uuid: 'test-uuid', type: 'item', _links: {self: {href: 'self-link'}}});
|
||||
const testCommunity: Community = Object.assign(new Community(), {
|
||||
uuid: 'test-community-uuid',
|
||||
type: 'community',
|
||||
_links: {self: {href: 'self-link'}},
|
||||
});
|
||||
const testCollection: Collection = Object.assign(new Collection(), {
|
||||
uuid: 'test-collection-uuid',
|
||||
type: 'collection',
|
||||
_links: {self: {href: 'self-link'}},
|
||||
});
|
||||
const testItem: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-item-uuid',
|
||||
type: 'item',
|
||||
_links: {self: {href: 'self-link'}},
|
||||
});
|
||||
|
||||
let testObject: DSpaceObject;
|
||||
let route;
|
||||
|
||||
const dummySections1 = [{
|
||||
id: 'dummy-1',
|
||||
@@ -90,6 +112,10 @@ describe('DSOEditMenuResolver', () => {
|
||||
}];
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// test with Items unless specified otherwise
|
||||
testObject = testItem;
|
||||
route = dsoRoute(testItem);
|
||||
|
||||
menuService = new MenuServiceStub();
|
||||
spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE));
|
||||
|
||||
@@ -154,16 +180,17 @@ describe('DSOEditMenuResolver', () => {
|
||||
{
|
||||
...route.data.menu,
|
||||
[MenuID.DSO_EDIT]: [
|
||||
...dummySections1.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'})),
|
||||
...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'}))
|
||||
...dummySections1.map((menu) => Object.assign(menu, {id: menu.id + '-test-item-uuid'})),
|
||||
...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-item-uuid'}))
|
||||
]
|
||||
}
|
||||
);
|
||||
expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-uuid', true, false);
|
||||
expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-item-uuid', true, false);
|
||||
expect(resolver.getDsoMenus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create all menus when a dso is found based on the route scope query param when no id param is present', (done) => {
|
||||
spyOn(resolver, 'getDsoMenus').and.returnValue(
|
||||
[observableOf(dummySections1), observableOf(dummySections2)]
|
||||
@@ -198,6 +225,7 @@ describe('DSOEditMenuResolver', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the statistics menu when no dso is found', (done) => {
|
||||
(dSpaceObjectDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
|
||||
@@ -211,49 +239,165 @@ describe('DSOEditMenuResolver', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDsoMenus', () => {
|
||||
it('should return as first part the item version, orcid and claim list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
result[0].subscribe((menuList) => {
|
||||
expect(menuList.length).toEqual(3);
|
||||
expect(menuList[0].id).toEqual('orcid-dso');
|
||||
expect(menuList[0].active).toEqual(false);
|
||||
// Visible should be false due to the item not being of type person
|
||||
expect(menuList[0].visible).toEqual(false);
|
||||
expect(menuList[0].model.type).toEqual(MenuItemType.LINK);
|
||||
|
||||
expect(menuList[1].id).toEqual('version-dso');
|
||||
expect(menuList[1].active).toEqual(false);
|
||||
expect(menuList[1].visible).toEqual(true);
|
||||
expect(menuList[1].model.type).toEqual(MenuItemType.ONCLICK);
|
||||
expect((menuList[1].model as TextMenuItemModel).text).toEqual('message');
|
||||
expect(menuList[1].model.disabled).toEqual(false);
|
||||
expect(menuList[1].icon).toEqual('code-branch');
|
||||
|
||||
expect(menuList[2].id).toEqual('claim-dso');
|
||||
expect(menuList[2].active).toEqual(false);
|
||||
// Visible should be false due to the item not being of type person
|
||||
expect(menuList[2].visible).toEqual(false);
|
||||
expect(menuList[2].model.type).toEqual(MenuItemType.ONCLICK);
|
||||
expect((menuList[2].model as TextMenuItemModel).text).toEqual('item.page.claim.button');
|
||||
done();
|
||||
describe('for Communities', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testCommunity;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCommunity));
|
||||
route = dsoRoute(testCommunity);
|
||||
});
|
||||
|
||||
it('should not return Item-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeFalsy();
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeFalsy();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeFalsy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Community/Collection-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeTruthy();
|
||||
expect(subscribeEntry.active).toBeFalse();
|
||||
expect(subscribeEntry.visible).toBeTrue();
|
||||
expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/communities/test-community-uuid/edit/metadata'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should return as second part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
result[1].subscribe((menuList) => {
|
||||
expect(menuList.length).toEqual(1);
|
||||
expect(menuList[0].id).toEqual('edit-dso');
|
||||
expect(menuList[0].active).toEqual(false);
|
||||
expect(menuList[0].visible).toEqual(true);
|
||||
expect(menuList[0].model.type).toEqual(MenuItemType.LINK);
|
||||
expect((menuList[0].model as LinkMenuItemModel).text).toEqual('item.page.edit');
|
||||
expect((menuList[0].model as LinkMenuItemModel).link).toEqual('/items/test-uuid/edit/metadata');
|
||||
expect(menuList[0].icon).toEqual('pencil-alt');
|
||||
done();
|
||||
|
||||
describe('for Collections', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testCollection;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCollection));
|
||||
route = dsoRoute(testCollection);
|
||||
});
|
||||
|
||||
it('should not return Item-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeFalsy();
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeFalsy();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeFalsy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Community/Collection-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeTruthy();
|
||||
expect(subscribeEntry.active).toBeFalse();
|
||||
expect(subscribeEntry.visible).toBeTrue();
|
||||
expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/collections/test-collection-uuid/edit/metadata'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for Items', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testItem;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testItem));
|
||||
route = dsoRoute(testItem);
|
||||
});
|
||||
|
||||
it('should return Item-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeTruthy();
|
||||
expect(orcidEntry.active).toBeFalse();
|
||||
expect(orcidEntry.visible).toBeFalse();
|
||||
expect(orcidEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeTruthy();
|
||||
expect(versionEntry.active).toBeFalse();
|
||||
expect(versionEntry.visible).toBeTrue();
|
||||
expect(versionEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
expect(versionEntry.model.disabled).toBeFalse();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeTruthy();
|
||||
expect(claimEntry.active).toBeFalse();
|
||||
expect(claimEntry.visible).toBeFalse();
|
||||
expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return Community/Collection-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/items/test-item-uuid/edit/metadata'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -21,6 +21,9 @@ import { getDSORoute } from '../../app-routing-paths';
|
||||
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
|
||||
/**
|
||||
* Creates the menus for the dspace object pages
|
||||
@@ -84,6 +87,7 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
|
||||
getDsoMenus(dso, route, state): Observable<MenuSection[]>[] {
|
||||
return [
|
||||
this.getItemMenu(dso),
|
||||
this.getComColMenu(dso),
|
||||
this.getCommonMenu(dso, state)
|
||||
];
|
||||
}
|
||||
@@ -178,6 +182,39 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Community/Collection-specific menus
|
||||
*/
|
||||
protected getComColMenu(dso): Observable<MenuSection[]> {
|
||||
if (dso instanceof Community || dso instanceof Collection) {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self),
|
||||
]).pipe(
|
||||
map(([canSubscribe]) => {
|
||||
return [
|
||||
{
|
||||
id: 'subscribe',
|
||||
active: false,
|
||||
visible: canSubscribe,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'subscriptions.tooltip',
|
||||
function: () => {
|
||||
const modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||
modalRef.componentInstance.dso = dso;
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'bell',
|
||||
index: 4
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return observableOf([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a researcher by creating a profile
|
||||
* Shows notifications and/or hides the menu section on success/error
|
||||
|
@@ -13,7 +13,6 @@ import { hasValue } from '../../../empty.util';
|
||||
* Represents an expandable section in the dso edit menus
|
||||
*/
|
||||
@Component({
|
||||
/* tslint:disable:component-selector */
|
||||
selector: 'ds-dso-edit-menu-expandable-section',
|
||||
templateUrl: './dso-edit-menu-expandable-section.component.html',
|
||||
styleUrls: ['./dso-edit-menu-expandable-section.component.scss'],
|
||||
|
@@ -10,7 +10,6 @@ import { MenuSection } from '../../../menu/menu-section.model';
|
||||
* Represents a non-expandable section in the dso edit menus
|
||||
*/
|
||||
@Component({
|
||||
/* tslint:disable:component-selector */
|
||||
selector: 'ds-dso-edit-menu-section',
|
||||
templateUrl: './dso-edit-menu-section.component.html',
|
||||
styleUrls: ['./dso-edit-menu-section.component.scss']
|
||||
|
@@ -1,8 +0,0 @@
|
||||
<button *ngIf="isAuthorized$ | async" data-test="subscription-button"
|
||||
(click)="openSubscriptionModal()"
|
||||
[ngbTooltip]="'subscriptions.tooltip' | translate"
|
||||
[title]="'subscriptions.tooltip' | translate"
|
||||
[attr.aria-label]="'subscriptions.tooltip' | translate"
|
||||
class="subscription-button btn btn-dark btn-sm">
|
||||
<i class="fas fa-bell fa-fw"></i>
|
||||
</button>
|
@@ -1,83 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DsoPageSubscriptionButtonComponent } from './dso-page-subscription-button.component';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||
|
||||
describe('DsoPageSubscriptionButtonComponent', () => {
|
||||
let component: DsoPageSubscriptionButtonComponent;
|
||||
let fixture: ComponentFixture<DsoPageSubscriptionButtonComponent>;
|
||||
let de: DebugElement;
|
||||
|
||||
const authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: jasmine.createSpy('isAuthorized') // observableOf(true)
|
||||
});
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018',
|
||||
type: ITEM,
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://localhost:8000/items/fake-id'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbModalModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})
|
||||
],
|
||||
declarations: [ DsoPageSubscriptionButtonComponent ],
|
||||
providers: [
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DsoPageSubscriptionButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
de = fixture.debugElement;
|
||||
component.dso = mockItem;
|
||||
});
|
||||
|
||||
describe('when is authorized', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized.and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display subscription button', () => {
|
||||
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized.and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not display subscription button', () => {
|
||||
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,57 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-page-subscription-button',
|
||||
templateUrl: './dso-page-subscription-button.component.html',
|
||||
styleUrls: ['./dso-page-subscription-button.component.scss']
|
||||
})
|
||||
/**
|
||||
* Display a button that opens the modal to manage subscriptions
|
||||
*/
|
||||
export class DsoPageSubscriptionButtonComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Whether the current user is authorized to edit the DSpaceObject
|
||||
*/
|
||||
isAuthorized$: Observable<boolean> = of(false);
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
public modalRef: NgbModalRef;
|
||||
|
||||
/**
|
||||
* DSpaceObject that is being viewed
|
||||
*/
|
||||
@Input() dso: DSpaceObject;
|
||||
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the current DSpaceObject can be subscribed by the user
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanSubscribe, this.dso.self);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal to subscribe to the related DSpaceObject
|
||||
*/
|
||||
public openSubscriptionModal() {
|
||||
this.modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||
this.modalRef.componentInstance.dso = this.dso;
|
||||
}
|
||||
|
||||
}
|
@@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AlertType } from '../alert/aletr-type';
|
||||
import { AlertType } from '../alert/alert-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-error',
|
||||
|
@@ -130,7 +130,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
||||
(v) => v.value === option.value));
|
||||
|
||||
const item: ListItem = {
|
||||
id: value,
|
||||
id: `${this.model.id}_${value}`,
|
||||
label: option.display,
|
||||
value: checked,
|
||||
index: key
|
||||
|
@@ -43,7 +43,7 @@
|
||||
|
||||
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
|
||||
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
|
||||
(click)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
||||
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
||||
title="{{ listEntry.display }}" role="option"
|
||||
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
||||
{{inputFormatter(listEntry)}}
|
||||
|
@@ -159,14 +159,15 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
|
||||
let de: any = scrollableDropdownFixture.debugElement.query(By.css('input.form-control'));
|
||||
let btnEl = de.nativeElement;
|
||||
|
||||
btnEl.click();
|
||||
const mousedownEvent = new MouseEvent('mousedown');
|
||||
|
||||
btnEl.dispatchEvent(mousedownEvent);
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
||||
btnEl = de[0].nativeElement;
|
||||
|
||||
btnEl.click();
|
||||
|
||||
btnEl.dispatchEvent(mousedownEvent);
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
|
||||
|
@@ -42,6 +42,7 @@
|
||||
[collection]="collection"
|
||||
[relationship]="relationshipOptions"
|
||||
[context]="context"
|
||||
[query]="query"
|
||||
[externalSource]="source"
|
||||
(importedObject)="imported($event)"
|
||||
class="d-block pt-3">
|
||||
|
@@ -75,6 +75,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
||||
* The context to displaying lists for
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The search query
|
||||
*/
|
||||
@Input() query: string;
|
||||
|
||||
@Input() repeatable: boolean;
|
||||
/**
|
||||
* Emit an event when an object has been imported (or selected from similar local entries)
|
||||
@@ -149,8 +155,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
||||
|
||||
this.resetRoute();
|
||||
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
switchMap((searchOptions: PaginatedSearchOptions) =>
|
||||
this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined)))
|
||||
switchMap((searchOptions: PaginatedSearchOptions) => {
|
||||
if (searchOptions.query === '') {
|
||||
searchOptions.query = this.query;
|
||||
}
|
||||
return this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined));
|
||||
})
|
||||
);
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination);
|
||||
this.importConfig = {
|
||||
|
@@ -15,7 +15,7 @@ import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-loo
|
||||
})
|
||||
export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent<DsDynamicLookupRelationExternalSourceTabComponent> {
|
||||
protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId',
|
||||
'item', 'collection', 'relationship', 'context', 'repeatable', 'importedObject', 'externalSource'];
|
||||
'item', 'collection', 'relationship', 'context', 'query', 'repeatable', 'importedObject', 'externalSource'];
|
||||
|
||||
@Input() label: string;
|
||||
|
||||
@@ -29,6 +29,8 @@ export class ThemedDynamicLookupRelationExternalSourceTabComponent extends Theme
|
||||
|
||||
@Input() context: Context;
|
||||
|
||||
@Input() query: string;
|
||||
|
||||
@Input() repeatable: boolean;
|
||||
|
||||
@Output() importedObject: EventEmitter<ListableObject> = new EventEmitter();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core';
|
||||
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
@@ -28,7 +28,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
|
||||
templateUrl: './vocabulary-treeview.component.html',
|
||||
styleUrls: ['./vocabulary-treeview.component.scss']
|
||||
})
|
||||
export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
||||
export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* The {@link VocabularyOptions} object
|
||||
@@ -322,4 +322,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
||||
private getEntryId(entry: VocabularyEntry): string {
|
||||
return entry.authority || entry.otherInformation.id || undefined;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.reset();
|
||||
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, null);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ul class="navbar-nav" *ngIf="(isAuthenticated$ | async) && isImpersonating">
|
||||
<ul class="navbar-nav" *ngIf="isImpersonating$ | async">
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-sm btn-dark" ngbTooltip="{{'nav.stop-impersonating' | translate}}" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i>
|
||||
|
@@ -14,6 +14,7 @@ import { authReducer } from '../../core/auth/auth.reducer';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { EPersonMock } from '../testing/eperson.mock';
|
||||
import { AppState, storeModuleConfig } from '../../app.reducer';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('ImpersonateNavbarComponent', () => {
|
||||
let component: ImpersonateNavbarComponent;
|
||||
@@ -65,7 +66,7 @@ describe('ImpersonateNavbarComponent', () => {
|
||||
|
||||
describe('when the user is impersonating another user', () => {
|
||||
beforeEach(() => {
|
||||
component.isImpersonating = true;
|
||||
component.isImpersonating$ = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isAuthenticated } from '../../core/auth/selectors';
|
||||
|
||||
@Component({
|
||||
@@ -13,24 +14,32 @@ import { isAuthenticated } from '../../core/auth/selectors';
|
||||
* Navbar component for actions to take concerning impersonating users
|
||||
*/
|
||||
export class ImpersonateNavbarComponent implements OnInit {
|
||||
/**
|
||||
* Whether or not the user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Is the user currently impersonating another user?
|
||||
*/
|
||||
isImpersonating: boolean;
|
||||
isImpersonating$: Observable<boolean>;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private authService: AuthService) {
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected elRef: ElementRef,
|
||||
protected store: Store<AppState>,
|
||||
protected authService: AuthService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
|
||||
this.isImpersonating = this.authService.isImpersonating();
|
||||
this.isImpersonating$ = this.store.pipe(select(isAuthenticated)).pipe(
|
||||
map((isUserAuthenticated: boolean) => isUserAuthenticated && this.authService.isImpersonating()),
|
||||
);
|
||||
this.subscriptions.push(this.isImpersonating$.subscribe((isImpersonating: boolean) => {
|
||||
if (isImpersonating) {
|
||||
this.elRef.nativeElement.classList.remove('d-none');
|
||||
} else {
|
||||
this.elRef.nativeElement.classList.add('d-none');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div ngbDropdown class="navbar-nav" *ngIf="moreThanOneLanguage" display="dynamic" placement="bottom-right">
|
||||
<a href="javascript:void(0);" role="button"
|
||||
[attr.aria-label]="'nav.language' |translate"
|
||||
[title]="'nav.language' | translate" class="px-1"
|
||||
[title]="'nav.language' | translate"
|
||||
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
||||
tabindex="0">
|
||||
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
||||
|
@@ -9,3 +9,7 @@
|
||||
color: var(--ds-header-icon-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { LangConfig } from '../../../config/lang-config.interface';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { LocaleService } from '../../core/locale/locale.service';
|
||||
@@ -25,6 +23,7 @@ export class LangSwitchComponent implements OnInit {
|
||||
moreThanOneLanguage: boolean;
|
||||
|
||||
constructor(
|
||||
public el: ElementRef,
|
||||
public translate: TranslateService,
|
||||
private localeService: LocaleService
|
||||
) {
|
||||
@@ -33,6 +32,9 @@ export class LangSwitchComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.activeLangs = environment.languages.filter((MyLangConfig) => MyLangConfig.active === true);
|
||||
this.moreThanOneLanguage = (this.activeLangs.length > 1);
|
||||
if (!this.moreThanOneLanguage) {
|
||||
this.el.nativeElement.parentElement.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
27
src/app/shared/lang-switch/themed-lang-switch.component.ts
Normal file
27
src/app/shared/lang-switch/themed-lang-switch.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../theme-support/themed.component';
|
||||
import { LangSwitchComponent } from './lang-switch.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for {@link LangSwitchComponent}
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-lang-switch',
|
||||
styleUrls: [],
|
||||
templateUrl: '../theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedLangSwitchComponent extends ThemedComponent<LangSwitchComponent> {
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'LangSwitchComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/shared/lang-switch/lang-switch.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./lang-switch.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -13,13 +13,17 @@ import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
import { AuthServiceStub } from '../../testing/auth-service.stub';
|
||||
import { createTestComponent } from '../../testing/utils.test';
|
||||
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
describe('LogInContainerComponent', () => {
|
||||
|
||||
let component: LogInContainerComponent;
|
||||
let fixture: ComponentFixture<LogInContainerComponent>;
|
||||
|
||||
const authMethod = new AuthMethod('password');
|
||||
const authMethod = new AuthMethod(AuthMethodType.Password, 0);
|
||||
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
@@ -35,13 +39,15 @@ describe('LogInContainerComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot()
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule,
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
LogInContainerComponent
|
||||
],
|
||||
@@ -113,6 +119,6 @@ describe('LogInContainerComponent', () => {
|
||||
class TestComponent {
|
||||
|
||||
isStandalonePage = true;
|
||||
authMethod = new AuthMethod('password');
|
||||
authMethod = new AuthMethod(AuthMethodType.Password, 0);
|
||||
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
<ds-themed-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-themed-loading>
|
||||
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 mx-auto login-container">
|
||||
<ng-container *ngFor="let authMethod of (authMethods); let i = index">
|
||||
<div *ngIf="i === 1" class="text-center mt-2">
|
||||
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
|
||||
<ng-container *ngFor="let authMethod of getOrderedAuthMethods(authMethods | async); let last = last">
|
||||
<div [class.d-none]="contentRef.innerText?.trim().length === 0">
|
||||
<div #contentRef>
|
||||
<ds-log-in-container [authMethod]="authMethod" [isStandalonePage]="isStandalonePage"></ds-log-in-container>
|
||||
</div>
|
||||
<div *ngIf="!last" class="dropdown-divider my-2"></div>
|
||||
</div>
|
||||
<ds-log-in-container [authMethod]="authMethod" [isStandalonePage]="isStandalonePage"></ds-log-in-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">{{"login.form.new-user" | translate}}</a>
|
||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">{{"login.form.forgot-password" | translate}}</a>
|
||||
</div>
|
||||
|
@@ -50,7 +50,7 @@ describe('LogInComponent', () => {
|
||||
});
|
||||
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
import {
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
isAuthenticated,
|
||||
isAuthenticationLoading
|
||||
} from '../../core/auth/selectors';
|
||||
import { getForgotPasswordRoute, getRegisterRoute } from '../../app-routing-paths';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { CoreState } from '../../core/core-state.model';
|
||||
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
||||
|
||||
@@ -23,7 +20,8 @@ import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
||||
@Component({
|
||||
selector: 'ds-log-in',
|
||||
templateUrl: './log-in.component.html',
|
||||
styleUrls: ['./log-in.component.scss']
|
||||
styleUrls: ['./log-in.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LogInComponent implements OnInit {
|
||||
|
||||
@@ -37,7 +35,7 @@ export class LogInComponent implements OnInit {
|
||||
* The list of authentication methods available
|
||||
* @type {AuthMethod[]}
|
||||
*/
|
||||
public authMethods: AuthMethod[];
|
||||
public authMethods: Observable<AuthMethod[]>;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
@@ -51,24 +49,17 @@ export class LogInComponent implements OnInit {
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Whether or not the current user (or anonymous) is authorized to register an account
|
||||
*/
|
||||
canRegister$: Observable<boolean>;
|
||||
|
||||
constructor(private store: Store<CoreState>,
|
||||
private authService: AuthService,
|
||||
private authorizationService: AuthorizationDataService) {
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.store.pipe(
|
||||
this.authMethods = this.store.pipe(
|
||||
select(getAuthenticationMethods),
|
||||
).subscribe(methods => {
|
||||
// ignore the ip authentication method when it's returned by the backend
|
||||
this.authMethods = methods.filter(a => a.authMethodType !== AuthMethodType.Ip);
|
||||
});
|
||||
map((methods: AuthMethod[]) => methods.filter((authMethod: AuthMethod) => authMethod.authMethodType !== AuthMethodType.Ip)),
|
||||
);
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
@@ -82,15 +73,18 @@ export class LogInComponent implements OnInit {
|
||||
this.authService.clearRedirectUrl();
|
||||
}
|
||||
});
|
||||
|
||||
this.canRegister$ = this.authorizationService.isAuthorized(FeatureID.EPersonRegistration);
|
||||
}
|
||||
|
||||
getRegisterRoute() {
|
||||
return getRegisterRoute();
|
||||
}
|
||||
|
||||
getForgotRoute() {
|
||||
return getForgotPasswordRoute();
|
||||
/**
|
||||
* Returns an ordered list of {@link AuthMethod}s based on their position.
|
||||
*
|
||||
* @param authMethods The {@link AuthMethod}s to sort
|
||||
*/
|
||||
getOrderedAuthMethods(authMethods: AuthMethod[] | null): AuthMethod[] {
|
||||
if (hasValue(authMethods)) {
|
||||
return [...authMethods].sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToExternalProvider()">
|
||||
<button class="btn btn-lg btn-primary btn-block text-white" (click)="redirectToExternalProvider()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{getButtonLabel() | translate}}
|
||||
</button>
|
||||
|
@@ -3,11 +3,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson.mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||
@@ -25,17 +22,14 @@ describe('LogInExternalProviderComponent', () => {
|
||||
|
||||
let component: LogInExternalProviderComponent;
|
||||
let fixture: ComponentFixture<LogInExternalProviderComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
let orcidBaseUrl;
|
||||
let location;
|
||||
let orcidBaseUrl: string;
|
||||
let location: string;
|
||||
let initialState: any;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl=';
|
||||
location = orcidBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
@@ -59,7 +53,7 @@ describe('LogInExternalProviderComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||
TranslateModule.forRoot()
|
||||
@@ -69,7 +63,7 @@ describe('LogInExternalProviderComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, location) },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, 0, location) },
|
||||
{ provide: 'isStandalonePage', useValue: true },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
@@ -94,7 +88,6 @@ describe('LogInExternalProviderComponent', () => {
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
});
|
||||
@@ -130,25 +123,3 @@ describe('LogInExternalProviderComponent', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture<LogInExternalProviderComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -28,3 +28,12 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [attr.data-test]="'login-button' | dsBrowserOnly"
|
||||
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-2">
|
||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">
|
||||
{{ 'login.form.new-user' | translate }}
|
||||
</a>
|
||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">
|
||||
{{ 'login.form.forgot-password' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -11,3 +11,7 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
white-space: normal;
|
||||
padding: .25rem .75rem;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user