mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into edit-metadata-redesign-PR
This commit is contained in:
@@ -310,3 +310,11 @@ info:
|
||||
markdown:
|
||||
enabled: false
|
||||
mathjax: false
|
||||
|
||||
# Which vocabularies should be used for which search filters
|
||||
# and whether to show the filter in the search sidebar
|
||||
# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||
vocabularies:
|
||||
- filter: 'subject'
|
||||
vocabulary: 'srsc'
|
||||
enabled: true
|
||||
|
@@ -30,8 +30,9 @@
|
||||
"clean:log": "rimraf *.log*",
|
||||
"clean:json": "rimraf *.records.json",
|
||||
"clean:node": "rimraf node_modules",
|
||||
"clean:cli": "rimraf .angular/cache",
|
||||
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
||||
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node",
|
||||
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node",
|
||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||
|
@@ -266,6 +266,43 @@ describe('GroupFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit with name and description operations', () => {
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: 'testDescription'
|
||||
}, {
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
value: 'newGroupName'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should edit with description operations', () => {
|
||||
component.groupName.value = null;
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: 'testDescription'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should edit with name operations', () => {
|
||||
component.groupDescription.value = null;
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
value: 'newGroupName'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
|
@@ -346,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (hasValue(this.groupDescription.value)) {
|
||||
operations = [...operations, {
|
||||
op: 'replace',
|
||||
path: '/metadata/dc.description/0/value',
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: this.groupDescription.value
|
||||
}];
|
||||
}
|
||||
|
@@ -196,7 +196,24 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
authStatus.token = new AuthTokenInfo(accessToken);
|
||||
} else {
|
||||
authStatus.authenticated = false;
|
||||
authStatus.error = isNotEmpty(error) ? ((typeof error === 'string') ? JSON.parse(error) : error) : null;
|
||||
if (isNotEmpty(error)) {
|
||||
if (typeof error === 'string') {
|
||||
try {
|
||||
authStatus.error = JSON.parse(error);
|
||||
} catch (e) {
|
||||
console.error('Unknown auth error "', error, '" caused ', e);
|
||||
authStatus.error = {
|
||||
error: 'Unknown',
|
||||
message: 'Unknown auth error',
|
||||
status: 500,
|
||||
timestamp: Date.now(),
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
} else {
|
||||
authStatus.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return authStatus;
|
||||
}
|
||||
|
@@ -141,7 +141,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
||||
comp.selectedItem = currentValue;
|
||||
fixture.detectChanges();
|
||||
expect(comp.dataSource.data).toEqual([]);
|
||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID');
|
||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
||||
});
|
||||
|
||||
it('should should init component properly with init value as VocabularyEntry', () => {
|
||||
@@ -153,7 +153,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
||||
comp.selectedItem = currentValue;
|
||||
fixture.detectChanges();
|
||||
expect(comp.dataSource.data).toEqual([]);
|
||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID');
|
||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
||||
});
|
||||
|
||||
it('should call loadMore function', () => {
|
||||
|
@@ -2,14 +2,13 @@ import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { filter, find, startWith } from 'rxjs/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
|
||||
import { isAuthenticated } from '../../../core/auth/selectors';
|
||||
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
@@ -18,6 +17,7 @@ import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vo
|
||||
import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
|
||||
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
|
||||
import { CoreState } from '../../../core/core-state.model';
|
||||
import { lowerCase } from 'lodash/string';
|
||||
|
||||
/**
|
||||
* Component that show a hierarchical vocabulary in a tree view
|
||||
@@ -203,23 +203,15 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
||||
})
|
||||
);
|
||||
|
||||
const descriptionLabel = 'vocabulary-treeview.tree.description.' + this.vocabularyOptions.name;
|
||||
this.description = this.translate.get(descriptionLabel).pipe(
|
||||
filter((msg) => msg !== descriptionLabel),
|
||||
startWith('')
|
||||
this.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe(
|
||||
map((type) => lowerCase(type)),
|
||||
).subscribe(
|
||||
(type) => this.description = this.translate.get('vocabulary-treeview.info', { type })
|
||||
);
|
||||
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
this.loading = this.vocabularyTreeviewService.isLoading();
|
||||
|
||||
this.isAuthenticated.pipe(
|
||||
find((isAuth) => isAuth)
|
||||
).subscribe(() => {
|
||||
const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null;
|
||||
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId);
|
||||
});
|
||||
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<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 login-container">
|
||||
<ng-container *ngFor="let authMethod of (authMethods | async); let i = index">
|
||||
<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>
|
||||
</div>
|
||||
|
@@ -14,6 +14,7 @@ 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';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
@@ -36,7 +37,7 @@ export class LogInComponent implements OnInit {
|
||||
* The list of authentication methods available
|
||||
* @type {AuthMethod[]}
|
||||
*/
|
||||
public authMethods: Observable<AuthMethod[]>;
|
||||
public authMethods: AuthMethod[];
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
@@ -62,9 +63,12 @@ export class LogInComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.authMethods = this.store.pipe(
|
||||
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);
|
||||
});
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToExternalProvider()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{getButtonLabel() | translate}}
|
||||
</button>
|
@@ -14,18 +14,17 @@ import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||
import { storeModuleConfig } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInOrcidComponent } from './log-in-orcid.component';
|
||||
import { LogInExternalProviderComponent } from './log-in-external-provider.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
describe('LogInExternalProviderComponent', () => {
|
||||
|
||||
describe('LogInOrcidComponent', () => {
|
||||
|
||||
let component: LogInOrcidComponent;
|
||||
let fixture: ComponentFixture<LogInOrcidComponent>;
|
||||
let component: LogInExternalProviderComponent;
|
||||
let fixture: ComponentFixture<LogInExternalProviderComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
@@ -66,7 +65,7 @@ describe('LogInOrcidComponent', () => {
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInOrcidComponent
|
||||
LogInExternalProviderComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
@@ -88,7 +87,7 @@ describe('LogInOrcidComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInOrcidComponent);
|
||||
fixture = TestBed.createComponent(LogInExternalProviderComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
@@ -109,7 +108,7 @@ describe('LogInOrcidComponent', () => {
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOrcid();
|
||||
component.redirectToExternalProvider();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
@@ -124,7 +123,7 @@ describe('LogInOrcidComponent', () => {
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOrcid();
|
||||
component.redirectToExternalProvider();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
@@ -143,7 +142,7 @@ class Page {
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture<LogInOrcidComponent>) {
|
||||
constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture<LogInExternalProviderComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
@@ -4,22 +4,27 @@ import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||
import { isEmpty, isNotNull } from '../../empty.util';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { CoreState } from '../../../core/core-state.model';
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isEmpty, isNotNull } from '../../../empty.util';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
||||
import { CoreState } from '../../../../core/core-state.model';
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-external-provider',
|
||||
template: ''
|
||||
|
||||
templateUrl: './log-in-external-provider.component.html',
|
||||
styleUrls: ['./log-in-external-provider.component.scss']
|
||||
})
|
||||
export abstract class LogInExternalProviderComponent implements OnInit {
|
||||
@renderAuthMethodFor(AuthMethodType.Oidc)
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
@renderAuthMethodFor(AuthMethodType.Orcid)
|
||||
export class LogInExternalProviderComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
@@ -107,4 +112,7 @@ export abstract class LogInExternalProviderComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
getButtonLabel() {
|
||||
return `login.form.${this.authMethod.authMethodType}`;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOidc()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{"login.form.oidc" | translate}}
|
||||
</button>
|
@@ -1,155 +0,0 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
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 { 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';
|
||||
import { storeModuleConfig } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInOidcComponent } from './log-in-oidc.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
|
||||
describe('LogInOidcComponent', () => {
|
||||
|
||||
let component: LogInOidcComponent;
|
||||
let fixture: ComponentFixture<LogInOidcComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
let oidcBaseUrl;
|
||||
let location;
|
||||
let initialState: any;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
oidcBaseUrl = 'dspace-rest.test/oidc?redirectUrl=';
|
||||
location = oidcBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
getCurrentRoute: {},
|
||||
redirect: {}
|
||||
});
|
||||
|
||||
initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
blocking: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInOidcComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Oidc, location) },
|
||||
{ provide: 'isStandalonePage', useValue: true },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
provideMockStore({ initialState }),
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInOidcComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
});
|
||||
|
||||
it('should set the properly a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOidc();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
it('should not set a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/home';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOidc();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 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: LogInOidcComponent, private fixture: ComponentFixture<LogInOidcComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-oidc',
|
||||
templateUrl: './log-in-oidc.component.html',
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Oidc)
|
||||
export class LogInOidcComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* Redirect to orcid authentication url
|
||||
*/
|
||||
redirectToOidc() {
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOrcid()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{"login.form.orcid" | translate}}
|
||||
</button>
|
@@ -1,21 +0,0 @@
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-orcid',
|
||||
templateUrl: './log-in-orcid.component.html',
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Orcid)
|
||||
export class LogInOrcidComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* Redirect to orcid authentication url
|
||||
*/
|
||||
redirectToOrcid() {
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToShibboleth()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{"login.form.shibboleth" | translate}}
|
||||
</button>
|
@@ -1,155 +0,0 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
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 { 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';
|
||||
import { storeModuleConfig } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInShibbolethComponent } from './log-in-shibboleth.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
|
||||
describe('LogInShibbolethComponent', () => {
|
||||
|
||||
let component: LogInShibbolethComponent;
|
||||
let fixture: ComponentFixture<LogInShibbolethComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
let shibbolethBaseUrl;
|
||||
let location;
|
||||
let initialState: any;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
shibbolethBaseUrl = 'dspace-rest.test/shibboleth?redirectUrl=';
|
||||
location = shibbolethBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
getCurrentRoute: {},
|
||||
redirect: {}
|
||||
});
|
||||
|
||||
initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
blocking: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInShibbolethComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Shibboleth, location) },
|
||||
{ provide: 'isStandalonePage', useValue: true },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
provideMockStore({ initialState }),
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInShibbolethComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
});
|
||||
|
||||
it('should set the properly a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
it('should not set a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/home';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 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: LogInShibbolethComponent, private fixture: ComponentFixture<LogInShibbolethComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
templateUrl: './log-in-shibboleth.component.html',
|
||||
styleUrls: ['./log-in-shibboleth.component.scss'],
|
||||
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
export class LogInShibbolethComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* Redirect to shibboleth authentication url
|
||||
*/
|
||||
redirectToShibboleth() {
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
@@ -29,3 +29,10 @@
|
||||
ngDefaultControl
|
||||
></ds-filter-input-suggestions>
|
||||
</div>
|
||||
|
||||
<a *ngIf="vocabularyExists$ | async"
|
||||
href="javascript:void(0);"
|
||||
id="show-{{filterConfig.name}}-tree"
|
||||
(click)="showVocabularyTree()">
|
||||
{{'search.filters.filter.show-tree' | translate: {name: ('search.filters.filter.' + filterConfig.name + '.head' | translate | lowercase )} }}
|
||||
</a>
|
||||
|
@@ -1,155 +1,155 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
||||
import { of as observableOf, BehaviorSubject } from 'rxjs';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterStub } from '../../../../testing/router.stub';
|
||||
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER,
|
||||
SearchFilterService
|
||||
SearchFilterService,
|
||||
REFRESH_FILTER
|
||||
} from '../../../../../core/shared/search/search-filter.service';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { SearchFiltersComponent } from '../../search-filters.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../../testing/router.stub';
|
||||
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
|
||||
import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { FacetValue} from '../../../models/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
FilterInputSuggestionsComponent
|
||||
} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
||||
import { FacetValue } from '../../../models/facet-value.model';
|
||||
import { FilterType } from '../../../models/filter-type.model';
|
||||
import { createPaginatedList } from '../../../../testing/utils.test';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||
|
||||
describe('SearchHierarchyFilterComponent', () => {
|
||||
let comp: SearchHierarchyFilterComponent;
|
||||
|
||||
let fixture: ComponentFixture<SearchHierarchyFilterComponent>;
|
||||
let searchService: SearchService;
|
||||
let router;
|
||||
let showVocabularyTreeLink: DebugElement;
|
||||
|
||||
const value1 = 'testvalue1';
|
||||
const value2 = 'test2';
|
||||
const value3 = 'another value3';
|
||||
const values: FacetValue[] = [
|
||||
{
|
||||
label: value1,
|
||||
value: value1,
|
||||
count: 52,
|
||||
_links: {
|
||||
self: {
|
||||
href: ''
|
||||
},
|
||||
search: {
|
||||
href: ''
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: value2,
|
||||
value: value2,
|
||||
count: 20,
|
||||
_links: {
|
||||
self: {
|
||||
href: ''
|
||||
},
|
||||
search: {
|
||||
href: ''
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: value3,
|
||||
value: value3,
|
||||
count: 5,
|
||||
_links: {
|
||||
self: {
|
||||
href: ''
|
||||
},
|
||||
search: {
|
||||
href: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const mockValues = createSuccessfulRemoteDataObject$(createPaginatedList(values));
|
||||
|
||||
const searchFilterServiceStub = {
|
||||
getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable<string[]> {
|
||||
return observableOf(values.map((value: FacetValue) => value.value));
|
||||
},
|
||||
getPage(_paramName: string): Observable<number> {
|
||||
return observableOf(0);
|
||||
},
|
||||
resetPage(_filterName: string): void {
|
||||
// empty
|
||||
}
|
||||
const testSearchLink = 'test-search';
|
||||
const testSearchFilter = 'test-search-filter';
|
||||
const VocabularyTreeViewComponent = {
|
||||
select: new EventEmitter<VocabularyEntryDetail>(),
|
||||
};
|
||||
|
||||
const remoteDataBuildServiceStub = {
|
||||
aggregate(_input: Observable<RemoteData<FacetValue>>[]): Observable<RemoteData<PaginatedList<FacetValue>[]>> {
|
||||
return createSuccessfulRemoteDataObject$([createPaginatedList(values)]);
|
||||
const searchService = {
|
||||
getSearchLink: () => testSearchLink,
|
||||
getFacetValuesFor: () => observableOf([]),
|
||||
};
|
||||
const searchFilterService = {
|
||||
getPage: () => observableOf(0),
|
||||
};
|
||||
const router = new RouterStub();
|
||||
const ngbModal = jasmine.createSpyObj('modal', {
|
||||
open: {
|
||||
componentInstance: VocabularyTreeViewComponent,
|
||||
}
|
||||
});
|
||||
const vocabularyService = {
|
||||
searchTopEntries: () => undefined,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgbModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
SearchHierarchyFilterComponent,
|
||||
SearchFiltersComponent,
|
||||
FilterInputSuggestionsComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: new SearchServiceStub() },
|
||||
{ provide: SearchFilterService, useValue: searchFilterServiceStub },
|
||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildServiceStub },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
{ provide: SearchFilterService, useValue: searchFilterService },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: NgbModal, useValue: ngbModal },
|
||||
{ provide: VocabularyService, useValue: vocabularyService },
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) }
|
||||
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false)}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(SearchHierarchyFilterComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
})
|
||||
;
|
||||
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||
name: 'filterName1',
|
||||
filterType: FilterType.text,
|
||||
hasFacets: false,
|
||||
isOpenByDefault: false,
|
||||
pageSize: 2
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
function init() {
|
||||
fixture = TestBed.createComponent(SearchHierarchyFilterComponent);
|
||||
comp = fixture.componentInstance; // SearchHierarchyFilterComponent test instance
|
||||
comp.filterConfig = mockFilterConfig;
|
||||
searchService = (comp as any).searchService;
|
||||
// @ts-ignore
|
||||
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
|
||||
router = (comp as any).router;
|
||||
fixture.detectChanges();
|
||||
showVocabularyTreeLink = fixture.debugElement.query(By.css('a#show-test-search-filter-tree'));
|
||||
}
|
||||
|
||||
describe('if the vocabulary doesn\'t exist', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData(
|
||||
undefined, 0, 0, RequestEntryState.Error, undefined, undefined, 404
|
||||
)));
|
||||
init();
|
||||
});
|
||||
|
||||
it('should not show the vocabulary tree link', () => {
|
||||
expect(showVocabularyTreeLink).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to the correct filter with the query operator', () => {
|
||||
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true);
|
||||
describe('if the vocabulary exists', () => {
|
||||
|
||||
const searchQuery = 'MARVEL';
|
||||
comp.onSubmit(searchQuery);
|
||||
beforeEach(() => {
|
||||
spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData(
|
||||
undefined, 0, 0, RequestEntryState.Success, undefined, buildPaginatedList(new PageInfo(), []), 200
|
||||
)));
|
||||
init();
|
||||
});
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['', 'search'], Object({
|
||||
queryParams: Object({ [mockFilterConfig.paramName]: [...values.map((value: FacetValue) => `${value.value},equals`), `${searchQuery},query`] }),
|
||||
queryParamsHandling: 'merge'
|
||||
}));
|
||||
it('should show the vocabulary tree link', () => {
|
||||
expect(showVocabularyTreeLink).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when clicking the vocabulary tree link', () => {
|
||||
|
||||
const alreadySelectedValues = [
|
||||
'already-selected-value-1',
|
||||
'already-selected-value-2',
|
||||
];
|
||||
const newSelectedValue = 'new-selected-value';
|
||||
|
||||
beforeEach(async () => {
|
||||
showVocabularyTreeLink.nativeElement.click();
|
||||
fixture.componentInstance.selectedValues$ = observableOf(
|
||||
alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value }))
|
||||
);
|
||||
VocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), {
|
||||
value: newSelectedValue,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should open the vocabulary tree modal', () => {
|
||||
expect(ngbModal.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when selecting a value from the vocabulary tree', () => {
|
||||
|
||||
it('should add a new search filter to the existing search filters', () => {
|
||||
waitForAsync(() => expect(router.navigate).toHaveBeenCalledWith([testSearchLink], {
|
||||
queryParams: {
|
||||
[`f.${testSearchFilter}`]: [
|
||||
...alreadySelectedValues,
|
||||
newSelectedValue,
|
||||
].map((value => `${value},equals`)),
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,30 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FilterType } from '../../../models/filter-type.model';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||
import { FilterType } from '../../../models/filter-type.model';
|
||||
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { VocabularyTreeviewComponent } from '../../../../form/vocabulary-treeview/vocabulary-treeview.component';
|
||||
import {
|
||||
VocabularyEntryDetail
|
||||
} from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
IN_PLACE_SEARCH,
|
||||
SearchFilterService, REFRESH_FILTER
|
||||
} from '../../../../../core/shared/search/search-filter.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||
import { FacetValue } from '../../../models/facet-value.model';
|
||||
import { getFacetValueForType } from '../../../search.utils';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { addOperatorToFilterValue } from '../../../search.utils';
|
||||
|
||||
@Component({
|
||||
@@ -16,6 +39,23 @@ import { addOperatorToFilterValue } from '../../../search.utils';
|
||||
*/
|
||||
@renderFacetFor(FilterType.hierarchy)
|
||||
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected filterService: SearchFilterService,
|
||||
protected rdbs: RemoteDataBuildService,
|
||||
protected router: Router,
|
||||
protected modalService: NgbModal,
|
||||
protected vocabularyService: VocabularyService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>
|
||||
) {
|
||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
|
||||
}
|
||||
|
||||
vocabularyExists$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Submits a new active custom value to the filter from the input field
|
||||
* Overwritten method from parent component, adds the "query" operator to the received data before passing it on
|
||||
@@ -24,4 +64,59 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
||||
onSubmit(data: any) {
|
||||
super.onSubmit(addOperatorToFilterValue(data, 'query'));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.vocabularyExists$ = this.vocabularyService.searchTopEntries(
|
||||
this.getVocabularyEntry(), new PageInfo(), true, false,
|
||||
).pipe(
|
||||
filter(rd => rd.hasCompleted),
|
||||
take(1),
|
||||
map(rd => {
|
||||
return rd.hasSucceeded;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the vocabulary tree modal popup.
|
||||
* When an entry is selected, add the filter query to the search options.
|
||||
*/
|
||||
showVocabularyTree() {
|
||||
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, {
|
||||
size: 'lg',
|
||||
windowClass: 'treeview'
|
||||
});
|
||||
modalRef.componentInstance.vocabularyOptions = {
|
||||
name: this.getVocabularyEntry(),
|
||||
closed: true
|
||||
};
|
||||
modalRef.componentInstance.select.subscribe((detail: VocabularyEntryDetail) => {
|
||||
this.selectedValues$
|
||||
.pipe(take(1))
|
||||
.subscribe((selectedValues) => {
|
||||
this.router.navigate(
|
||||
[this.searchService.getSearchLink()],
|
||||
{
|
||||
queryParams: {
|
||||
[this.filterConfig.paramName]: [...selectedValues, {value: detail.value}]
|
||||
.map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)),
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matching vocabulary entry for the given search filter.
|
||||
* These are configurable in the config file.
|
||||
*/
|
||||
getVocabularyEntry() {
|
||||
const foundVocabularyConfig = environment.vocabularies.filter((v) => v.filter === this.filterConfig.name);
|
||||
if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) {
|
||||
return foundVocabularyConfig[0].vocabulary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -186,7 +186,6 @@ import {
|
||||
ImportableListItemControlComponent
|
||||
} from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
||||
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
|
||||
import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component';
|
||||
import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component';
|
||||
import { LogInComponent } from './log-in/log-in.component';
|
||||
import { MissingTranslationHelper } from './translate/missing-translation.helper';
|
||||
@@ -229,9 +228,7 @@ import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'
|
||||
import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component';
|
||||
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
|
||||
import { DsSelectComponent } from './ds-select/ds-select.component';
|
||||
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
|
||||
import { RSSComponent } from './rss-feed/rss.component';
|
||||
import { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component';
|
||||
import { BrowserOnlyPipe } from './utils/browser-only.pipe';
|
||||
import { ThemedLoadingComponent } from './loading/themed-loading.component';
|
||||
import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.component';
|
||||
@@ -246,6 +243,8 @@ import {
|
||||
} from './object-list/listable-notification-object/listable-notification-object.component';
|
||||
import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component';
|
||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { LogInExternalProviderComponent } from './log-in/methods/log-in-external-provider/log-in-external-provider.component';
|
||||
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -386,9 +385,7 @@ const ENTRY_COMPONENTS = [
|
||||
MetadataRepresentationListElementComponent,
|
||||
ItemMetadataRepresentationListElementComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInShibbolethComponent,
|
||||
LogInOidcComponent,
|
||||
LogInOrcidComponent,
|
||||
LogInExternalProviderComponent,
|
||||
CollectionDropdownComponent,
|
||||
ThemedCollectionDropdownComponent,
|
||||
FileDownloadLinkComponent,
|
||||
|
@@ -11,7 +11,7 @@ describe('Angulartics2DSpace', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
angulartics2 = {
|
||||
eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}),
|
||||
eventTrack: observableOf({action: 'page_view', properties: {object: 'mock-object'}}),
|
||||
filterDeveloperMode: () => filter(() => true)
|
||||
} as any;
|
||||
statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null});
|
||||
|
@@ -24,7 +24,7 @@ export class Angulartics2DSpace {
|
||||
}
|
||||
|
||||
private eventTrack(event) {
|
||||
if (event.action === 'pageView') {
|
||||
if (event.action === 'page_view') {
|
||||
this.statisticsService.trackViewEvent(event.properties.object);
|
||||
} else if (event.action === 'search') {
|
||||
this.statisticsService.trackSearchEvent(
|
||||
|
@@ -20,7 +20,7 @@ export class ViewTrackerComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.angulartics2.eventTrack.next({
|
||||
action: 'pageView',
|
||||
action: 'page_view',
|
||||
properties: {object: this.object},
|
||||
});
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2';
|
||||
import {
|
||||
Angulartics2GoogleAnalytics,
|
||||
Angulartics2GoogleGlobalSiteTag,
|
||||
} from 'angulartics2';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { GoogleAnalyticsService } from './google-analytics.service';
|
||||
@@ -16,7 +19,7 @@ describe('GoogleAnalyticsService', () => {
|
||||
const srcTestValue = 'mock-script-src';
|
||||
let service: GoogleAnalyticsService;
|
||||
let googleAnalyticsSpy: Angulartics2GoogleAnalytics;
|
||||
let googleTagManagerSpy: Angulartics2GoogleTagManager;
|
||||
let googleTagManagerSpy: Angulartics2GoogleGlobalSiteTag;
|
||||
let configSpy: ConfigurationDataService;
|
||||
let klaroServiceSpy: jasmine.SpyObj<KlaroService>;
|
||||
let scriptElementMock: any;
|
||||
@@ -37,7 +40,7 @@ describe('GoogleAnalyticsService', () => {
|
||||
googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [
|
||||
'startTracking',
|
||||
]);
|
||||
googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [
|
||||
googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleGlobalSiteTag', [
|
||||
'startTracking',
|
||||
]);
|
||||
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
|
||||
import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2';
|
||||
import {
|
||||
Angulartics2GoogleAnalytics,
|
||||
Angulartics2GoogleGlobalSiteTag,
|
||||
} from 'angulartics2';
|
||||
import { combineLatest } from 'rxjs';
|
||||
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
@@ -19,7 +22,7 @@ export class GoogleAnalyticsService {
|
||||
|
||||
constructor(
|
||||
private googleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private googleTagManager: Angulartics2GoogleTagManager,
|
||||
private googleGlobalSiteTag: Angulartics2GoogleGlobalSiteTag,
|
||||
private klaroService: KlaroService,
|
||||
private configService: ConfigurationDataService,
|
||||
@Inject(DOCUMENT) private document: any,
|
||||
@@ -70,7 +73,7 @@ export class GoogleAnalyticsService {
|
||||
this.document.body.appendChild(libScript);
|
||||
|
||||
// start tracking
|
||||
this.googleTagManager.startTracking();
|
||||
this.googleGlobalSiteTag.startTracking();
|
||||
} else {
|
||||
// add trackingId snippet to page
|
||||
const keyScript = this.document.createElement('script');
|
||||
|
@@ -3708,6 +3708,7 @@
|
||||
|
||||
"search.filters.filter.submitter.label": "Search submitter",
|
||||
|
||||
"search.filters.filter.show-tree": "Browse {{ name }} tree",
|
||||
|
||||
|
||||
"search.filters.entityType.JournalIssue": "Journal Issue",
|
||||
@@ -4582,7 +4583,7 @@
|
||||
|
||||
"vocabulary-treeview.tree.description.srsc": "Research Subject Categories",
|
||||
|
||||
|
||||
"vocabulary-treeview.info": "Select a subject to add as search filter",
|
||||
|
||||
"uploader.browse": "browse",
|
||||
|
||||
|
@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
|
||||
import { CommunityListConfig } from './community-list-config.interface';
|
||||
import { HomeConfig } from './homepage-config.interface';
|
||||
import { MarkdownConfig } from './markdown-config.interface';
|
||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||
|
||||
interface AppConfig extends Config {
|
||||
ui: UIServerConfig;
|
||||
@@ -44,6 +45,7 @@ interface AppConfig extends Config {
|
||||
actuators: ActuatorsConfig
|
||||
info: InfoConfig;
|
||||
markdown: MarkdownConfig;
|
||||
vocabularies: FilterVocabularyConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
|
||||
import { CommunityListConfig } from './community-list-config.interface';
|
||||
import { HomeConfig } from './homepage-config.interface';
|
||||
import { MarkdownConfig } from './markdown-config.interface';
|
||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||
|
||||
export class DefaultAppConfig implements AppConfig {
|
||||
production = false;
|
||||
@@ -385,4 +386,15 @@ export class DefaultAppConfig implements AppConfig {
|
||||
enabled: false,
|
||||
mathjax: false,
|
||||
};
|
||||
|
||||
// Which vocabularies should be used for which search filters
|
||||
// and whether to show the filter in the search sidebar
|
||||
// Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||
vocabularies: FilterVocabularyConfig[] = [
|
||||
{
|
||||
filter: 'subject',
|
||||
vocabulary: 'srsc',
|
||||
enabled: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
22
src/config/filter-vocabulary-config.ts
Normal file
22
src/config/filter-vocabulary-config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Config } from './config.interface';
|
||||
|
||||
/**
|
||||
* Configuration that can be used to enable a vocabulary tree to be used as search filter
|
||||
*/
|
||||
export interface FilterVocabularyConfig extends Config {
|
||||
/**
|
||||
* The name of the filter where the vocabulary tree should be used
|
||||
* This is the name of the filter as it's configured in the facet in discovery.xml
|
||||
* (can also be seen on the /server/api/discover/facets endpoint)
|
||||
*/
|
||||
filter: string;
|
||||
/**
|
||||
* name of the vocabulary tree to use
|
||||
* ( name of the file as stored in the dspace/config/controlled-vocabularies folder without file extension )
|
||||
*/
|
||||
vocabulary: string;
|
||||
/**
|
||||
* Whether to show the vocabulary tree in the sidebar
|
||||
*/
|
||||
enabled: boolean;
|
||||
}
|
@@ -283,4 +283,12 @@ export const environment: BuildConfig = {
|
||||
enabled: false,
|
||||
mathjax: false,
|
||||
},
|
||||
|
||||
vocabularies: [
|
||||
{
|
||||
filter: 'subject',
|
||||
vocabulary: 'srsc',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@@ -31,6 +31,7 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||
import { BrowserInitService } from './browser-init.service';
|
||||
import { VocabularyTreeviewService } from 'src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service';
|
||||
|
||||
export const REQ_KEY = makeStateKey<string>('req');
|
||||
|
||||
@@ -111,6 +112,10 @@ export function getRequest(transferState: TransferState): any {
|
||||
provide: LocationToken,
|
||||
useFactory: locationProvider,
|
||||
},
|
||||
{
|
||||
provide: VocabularyTreeviewService,
|
||||
useClass: VocabularyTreeviewService,
|
||||
}
|
||||
]
|
||||
})
|
||||
export class BrowserAppModule {
|
||||
|
@@ -6,7 +6,11 @@ import { ServerModule, ServerTransferStateModule } from '@angular/platform-serve
|
||||
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { Angulartics2, Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2';
|
||||
import {
|
||||
Angulartics2,
|
||||
Angulartics2GoogleAnalytics,
|
||||
Angulartics2GoogleGlobalSiteTag
|
||||
} from 'angulartics2';
|
||||
|
||||
import { AppComponent } from '../../app/app.component';
|
||||
|
||||
@@ -63,7 +67,7 @@ export function createTranslateLoader(transferState: TransferState) {
|
||||
useClass: AngularticsProviderMock
|
||||
},
|
||||
{
|
||||
provide: Angulartics2GoogleTagManager,
|
||||
provide: Angulartics2GoogleGlobalSiteTag,
|
||||
useClass: AngularticsProviderMock
|
||||
},
|
||||
{
|
||||
|
Reference in New Issue
Block a user