Merge branch 'main' into w2p-97184_theme-feedback_contribute-main

This commit is contained in:
lotte
2023-01-23 12:48:37 +01:00
46 changed files with 781 additions and 577 deletions

View File

@@ -310,3 +310,11 @@ info:
markdown: markdown:
enabled: false enabled: false
mathjax: 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

View File

@@ -30,8 +30,9 @@
"clean:log": "rimraf *.log*", "clean:log": "rimraf *.log*",
"clean:json": "rimraf *.records.json", "clean:json": "rimraf *.records.json",
"clean:node": "rimraf node_modules", "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: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", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",

View File

@@ -266,6 +266,43 @@ describe('GroupFormComponent', () => {
fixture.detectChanges(); 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(() => { it('should emit the existing group using the correct new values', waitForAsync(() => {
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);

View File

@@ -346,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
if (hasValue(this.groupDescription.value)) { if (hasValue(this.groupDescription.value)) {
operations = [...operations, { operations = [...operations, {
op: 'replace', op: 'add',
path: '/metadata/dc.description/0/value', path: '/metadata/dc.description',
value: this.groupDescription.value value: this.groupDescription.value
}]; }];
} }

View File

@@ -196,7 +196,24 @@ export class AuthInterceptor implements HttpInterceptor {
authStatus.token = new AuthTokenInfo(accessToken); authStatus.token = new AuthTokenInfo(accessToken);
} else { } else {
authStatus.authenticated = false; 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; return authStatus;
} }

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { hasValue, isEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty } from '../../shared/empty.util';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Metadata } from '../shared/metadata.utils';
/** /**
* Returns a name for a {@link DSpaceObject} based * Returns a name for a {@link DSpaceObject} based
@@ -67,4 +68,45 @@ export class DSONameService {
return name; return name;
} }
/**
* Gets the Hit highlight
*
* @param object
* @param dso
*
* @returns {string} html embedded hit highlight.
*/
getHitHighlights(object: any, dso: DSpaceObject): string {
const types = dso.getRenderTypes();
const entityType = types
.filter((type) => typeof type === 'string')
.find((type: string) => (['Person', 'OrgUnit']).includes(type)) as string;
if (entityType === 'Person') {
const familyName = this.firstMetadataValue(object, dso, 'person.familyName');
const givenName = this.firstMetadataValue(object, dso, 'person.givenName');
if (isEmpty(familyName) && isEmpty(givenName)) {
return this.firstMetadataValue(object, dso, 'dc.title') || dso.name;
} else if (isEmpty(familyName) || isEmpty(givenName)) {
return familyName || givenName;
}
return `${familyName}, ${givenName}`;
} else if (entityType === 'OrgUnit') {
return this.firstMetadataValue(object, dso, 'organization.legalName');
}
return this.firstMetadataValue(object, dso, 'dc.title') || dso.name || this.translateService.instant('dso.name.untitled');
}
/**
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
*
* @param object
* @param dso
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
*
* @returns {string} the first matching string value, or `undefined`.
*/
firstMetadataValue(object: any, dso: DSpaceObject, keyOrKeys: string | string[]): string {
return Metadata.firstValue([object.hitHighlights, dso.metadata], keyOrKeys);
}
} }

View File

@@ -61,7 +61,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants; this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants;
if (this.useNameVariants) { if (this.useNameVariants) {
const defaultValue = this.dsoTitle; const defaultValue = this.dso ? this.dsoNameService.getName(this.dso) : undefined;
const alternatives = this.allMetadataValues(this.alternativeField); const alternatives = this.allMetadataValues(this.alternativeField);
this.allSuggestions = [defaultValue, ...alternatives]; this.allSuggestions = [defaultValue, ...alternatives];

View File

@@ -55,7 +55,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
const defaultValue = this.dsoTitle; const defaultValue = this.dso ? this.dsoNameService.getName(this.dso) : undefined;
const alternatives = this.allMetadataValues(this.alternativeField); const alternatives = this.allMetadataValues(this.alternativeField);
this.allSuggestions = [defaultValue, ...alternatives]; this.allSuggestions = [defaultValue, ...alternatives];

View File

@@ -141,7 +141,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
comp.selectedItem = currentValue; comp.selectedItem = currentValue;
fixture.detectChanges(); fixture.detectChanges();
expect(comp.dataSource.data).toEqual([]); 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', () => { it('should should init component properly with init value as VocabularyEntry', () => {
@@ -153,7 +153,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
comp.selectedItem = currentValue; comp.selectedItem = currentValue;
fixture.detectChanges(); fixture.detectChanges();
expect(comp.dataSource.data).toEqual([]); 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', () => { it('should call loadMore function', () => {

View File

@@ -2,14 +2,13 @@ import { FlatTreeControl } from '@angular/cdk/tree';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 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 { Observable, Subscription } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
import { isAuthenticated } from '../../../core/auth/selectors';
import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.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 { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
import { CoreState } from '../../../core/core-state.model'; import { CoreState } from '../../../core/core-state.model';
import { lowerCase } from 'lodash/string';
/** /**
* Component that show a hierarchical vocabulary in a tree view * 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.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe(
this.description = this.translate.get(descriptionLabel).pipe( map((type) => lowerCase(type)),
filter((msg) => msg !== descriptionLabel), ).subscribe(
startWith('') (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.loading = this.vocabularyTreeviewService.isLoading();
this.isAuthenticated.pipe( this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null);
find((isAuth) => isAuth)
).subscribe(() => {
const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null;
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId);
});
} }
/** /**

View File

@@ -1,6 +1,6 @@
<ds-themed-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-themed-loading> <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"> <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"> <div *ngIf="i === 1" class="text-center mt-2">
<span class="align-middle">{{"login.form.or-divider" | translate}}</span> <span class="align-middle">{{"login.form.or-divider" | translate}}</span>
</div> </div>

View File

@@ -14,6 +14,7 @@ import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { CoreState } from '../../core/core-state.model'; import { CoreState } from '../../core/core-state.model';
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
/** /**
* /users/sign-in * /users/sign-in
@@ -36,7 +37,7 @@ export class LogInComponent implements OnInit {
* The list of authentication methods available * The list of authentication methods available
* @type {AuthMethod[]} * @type {AuthMethod[]}
*/ */
public authMethods: Observable<AuthMethod[]>; public authMethods: AuthMethod[];
/** /**
* Whether user is authenticated. * Whether user is authenticated.
@@ -62,9 +63,12 @@ export class LogInComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.authMethods = this.store.pipe( this.store.pipe(
select(getAuthenticationMethods), 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 // set loading
this.loading = this.store.pipe(select(isAuthenticationLoading)); this.loading = this.store.pipe(select(isAuthenticationLoading));

View File

@@ -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>

View File

@@ -14,18 +14,17 @@ import { AuthServiceStub } from '../../../testing/auth-service.stub';
import { storeModuleConfig } from '../../../../app.reducer'; import { storeModuleConfig } from '../../../../app.reducer';
import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; 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 { NativeWindowService } from '../../../../core/services/window.service';
import { RouterStub } from '../../../testing/router.stub'; import { RouterStub } from '../../../testing/router.stub';
import { ActivatedRouteStub } from '../../../testing/active-router.stub'; import { ActivatedRouteStub } from '../../../testing/active-router.stub';
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
describe('LogInExternalProviderComponent', () => {
describe('LogInOrcidComponent', () => { let component: LogInExternalProviderComponent;
let fixture: ComponentFixture<LogInExternalProviderComponent>;
let component: LogInOrcidComponent;
let fixture: ComponentFixture<LogInOrcidComponent>;
let page: Page; let page: Page;
let user: EPerson; let user: EPerson;
let componentAsAny: any; let componentAsAny: any;
@@ -66,7 +65,7 @@ describe('LogInOrcidComponent', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
LogInOrcidComponent LogInExternalProviderComponent
], ],
providers: [ providers: [
{ provide: AuthService, useClass: AuthServiceStub }, { provide: AuthService, useClass: AuthServiceStub },
@@ -88,7 +87,7 @@ describe('LogInOrcidComponent', () => {
beforeEach(() => { beforeEach(() => {
// create component and test fixture // create component and test fixture
fixture = TestBed.createComponent(LogInOrcidComponent); fixture = TestBed.createComponent(LogInExternalProviderComponent);
// get test component from the fixture // get test component from the fixture
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -109,7 +108,7 @@ describe('LogInOrcidComponent', () => {
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
component.redirectToOrcid(); component.redirectToExternalProvider();
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
@@ -124,7 +123,7 @@ describe('LogInOrcidComponent', () => {
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
component.redirectToOrcid(); component.redirectToExternalProvider();
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
@@ -143,7 +142,7 @@ class Page {
public navigateSpy: jasmine.Spy; public navigateSpy: jasmine.Spy;
public passwordInput: HTMLInputElement; public passwordInput: HTMLInputElement;
constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture<LogInOrcidComponent>) { constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture<LogInExternalProviderComponent>) {
// use injector to get services // use injector to get services
const injector = fixture.debugElement.injector; const injector = fixture.debugElement.injector;
const store = injector.get(Store); const store = injector.get(Store);

View File

@@ -4,22 +4,27 @@ import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { select, Store } from '@ngrx/store'; 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 { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
import { isEmpty, isNotNull } from '../../empty.util'; import { isEmpty, isNotNull } from '../../../empty.util';
import { AuthService } from '../../../core/auth/auth.service'; import { AuthService } from '../../../../core/auth/auth.service';
import { HardRedirectService } from '../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
import { CoreState } from '../../../core/core-state.model'; import { CoreState } from '../../../../core/core-state.model';
import { renderAuthMethodFor } from '../log-in.methods-decorator';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
@Component({ @Component({
selector: 'ds-log-in-external-provider', 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. * The authentication method data.
@@ -107,4 +112,7 @@ export abstract class LogInExternalProviderComponent implements OnInit {
} }
getButtonLabel() {
return `login.form.${this.authMethod.authMethodType}`;
}
} }

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -6,4 +6,23 @@ export class DSONameServiceMock {
public getName(dso: DSpaceObject) { public getName(dso: DSpaceObject) {
return UNDEFINED_NAME; return UNDEFINED_NAME;
} }
public getHitHighlights(object: any, dso: DSpaceObject) {
if (object.hitHighlights && object.hitHighlights['dc.title']) {
return object.hitHighlights['dc.title'][0].value;
} else if (object.hitHighlights && object.hitHighlights['organization.legalName']) {
return object.hitHighlights['organization.legalName'][0].value;
} else if (object.hitHighlights && (object.hitHighlights['person.familyName'] || object.hitHighlights['person.givenName'])) {
if (object.hitHighlights['person.familyName'] && object.hitHighlights['person.givenName']) {
return `${object.hitHighlights['person.familyName'][0].value}, ${object.hitHighlights['person.givenName'][0].value}`;
}
if (object.hitHighlights['person.familyName']) {
return `${object.hitHighlights['person.familyName'][0].value}`;
}
if (object.hitHighlights['person.givenName']) {
return `${object.hitHighlights['person.givenName'][0].value}`;
}
}
return UNDEFINED_NAME;
}
} }

View File

@@ -28,13 +28,19 @@ import { ItemSearchResultGridElementComponent } from './item-search-result-grid-
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
mockItemWithMetadata.hitHighlights = {}; mockItemWithMetadata.hitHighlights = {};
const dcTitle = 'This is just another <em>title</em>';
mockItemWithMetadata.indexableObject = Object.assign(new Item(), { mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
hitHighlights: {
'dc.title': [{
value: dcTitle
}],
},
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
metadata: { metadata: {
'dc.title': [ 'dc.title': [
{ {
language: 'en_US', language: 'en_US',
value: 'This is just another title' value: dcTitle
} }
], ],
'dc.contributor.author': [ 'dc.contributor.author': [
@@ -57,6 +63,114 @@ mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
] ]
} }
}); });
const mockPerson: ItemSearchResult = Object.assign(new ItemSearchResult(), {
hitHighlights: {
'person.familyName': [{
value: '<em>Michel</em>'
}],
},
indexableObject:
Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
entityType: 'Person',
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.publisher': [
{
language: 'en_US',
value: 'a publisher'
}
],
'dc.date.issued': [
{
language: 'en_US',
value: '2015-06-26'
}
],
'dc.description.abstract': [
{
language: 'en_US',
value: 'This is the abstract'
}
],
'dspace.entity.type': [
{
value: 'Person'
}
],
'person.familyName': [
{
value: 'Michel'
}
]
}
})
});
const mockOrgUnit: ItemSearchResult = Object.assign(new ItemSearchResult(), {
hitHighlights: {
'organization.legalName': [{
value: '<em>Science</em>'
}],
},
indexableObject:
Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
entityType: 'OrgUnit',
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.publisher': [
{
language: 'en_US',
value: 'a publisher'
}
],
'dc.date.issued': [
{
language: 'en_US',
value: '2015-06-26'
}
],
'dc.description.abstract': [
{
language: 'en_US',
value: 'This is the abstract'
}
],
'organization.legalName': [
{
value: 'Science'
}
],
'dspace.entity.type': [
{
value: 'OrgUnit'
}
]
}
})
});
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
mockItemWithoutMetadata.hitHighlights = {}; mockItemWithoutMetadata.hitHighlights = {};
@@ -154,6 +268,41 @@ export function getEntityGridElementTestComponent(component, searchResultWithMet
expect(itemAuthorField).toBeNull(); expect(itemAuthorField).toBeNull();
}); });
}); });
describe('When the item has title', () => {
beforeEach(() => {
comp.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.card-title'));
expect(titleField.nativeNode.innerHTML).toEqual(dcTitle);
});
});
describe('When the item is Person and has title', () => {
beforeEach(() => {
comp.object = mockPerson;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.card-title'));
expect(titleField.nativeNode.innerHTML).toEqual('<em>Michel</em>');
});
});
describe('When the item is orgUnit and has title', () => {
beforeEach(() => {
comp.object = mockOrgUnit;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.card-title'));
expect(titleField.nativeNode.innerHTML).toEqual('<em>Science</em>');
});
});
}); });
}; };
} }

View File

@@ -42,6 +42,6 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.itemPageRoute = getItemPageRoute(this.dso); this.itemPageRoute = getItemPageRoute(this.dso);
this.dsoTitle = this.dsoNameService.getName(this.dso); this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso);
} }
} }

View File

@@ -55,7 +55,7 @@ export class ItemListPreviewComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.showThumbnails = this.appConfig.browseBy.showThumbnails; this.showThumbnails = this.appConfig.browseBy.showThumbnails;
this.dsoTitle = this.dsoNameService.getName(this.item); this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.item);
} }

View File

@@ -13,8 +13,13 @@ import { APP_CONFIG } from '../../../../../../../config/app-config.interface';
let publicationListElementComponent: ItemSearchResultListElementComponent; let publicationListElementComponent: ItemSearchResultListElementComponent;
let fixture: ComponentFixture<ItemSearchResultListElementComponent>; let fixture: ComponentFixture<ItemSearchResultListElementComponent>;
const dcTitle = 'This is just another <em>title</em>';
const mockItemWithMetadata: ItemSearchResult = Object.assign(new ItemSearchResult(), { const mockItemWithMetadata: ItemSearchResult = Object.assign(new ItemSearchResult(), {
hitHighlights: {
'dc.title': [{
value: dcTitle
}],
},
indexableObject: indexableObject:
Object.assign(new Item(), { Object.assign(new Item(), {
bundles: observableOf({}), bundles: observableOf({}),
@@ -22,7 +27,7 @@ const mockItemWithMetadata: ItemSearchResult = Object.assign(new ItemSearchResul
'dc.title': [ 'dc.title': [
{ {
language: 'en_US', language: 'en_US',
value: 'This is just another title' value: dcTitle
} }
], ],
'dc.contributor.author': [ 'dc.contributor.author': [
@@ -59,7 +64,114 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(new ItemSearchRe
metadata: {} metadata: {}
}) })
}); });
const mockPerson: ItemSearchResult = Object.assign(new ItemSearchResult(), {
hitHighlights: {
'person.familyName': [{
value: '<em>Michel</em>'
}],
},
indexableObject:
Object.assign(new Item(), {
bundles: observableOf({}),
entityType: 'Person',
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.publisher': [
{
language: 'en_US',
value: 'a publisher'
}
],
'dc.date.issued': [
{
language: 'en_US',
value: '2015-06-26'
}
],
'dc.description.abstract': [
{
language: 'en_US',
value: 'This is the abstract'
}
],
'person.familyName': [
{
value: 'Michel'
}
],
'dspace.entity.type': [
{
value: 'Person'
}
]
}
})
});
const mockOrgUnit: ItemSearchResult = Object.assign(new ItemSearchResult(), {
hitHighlights: {
'organization.legalName': [{
value: '<em>Science</em>'
}],
},
indexableObject:
Object.assign(new Item(), {
bundles: observableOf({}),
entityType: 'OrgUnit',
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.publisher': [
{
language: 'en_US',
value: 'a publisher'
}
],
'dc.date.issued': [
{
language: 'en_US',
value: '2015-06-26'
}
],
'dc.description.abstract': [
{
language: 'en_US',
value: 'This is the abstract'
}
],
'organization.legalName': [
{
value: 'Science'
}
],
'dspace.entity.type': [
{
value: 'OrgUnit'
}
]
}
})
});
const environmentUseThumbs = { const environmentUseThumbs = {
browseBy: { browseBy: {
showThumbnails: true showThumbnails: true
@@ -205,6 +317,42 @@ describe('ItemSearchResultListElementComponent', () => {
}); });
}); });
describe('When the item has title', () => {
beforeEach(() => {
publicationListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.item-list-title'));
expect(titleField.nativeNode.innerHTML).toEqual(dcTitle);
});
});
describe('When the item is Person and has title', () => {
beforeEach(() => {
publicationListElementComponent.object = mockPerson;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.item-list-title'));
expect(titleField.nativeNode.innerHTML).toEqual('<em>Michel</em>');
});
});
describe('When the item is orgUnit and has title', () => {
beforeEach(() => {
publicationListElementComponent.object = mockOrgUnit;
fixture.detectChanges();
});
it('should show highlighted title', () => {
const titleField = fixture.debugElement.query(By.css('.item-list-title'));
expect(titleField.nativeNode.innerHTML).toEqual('<em>Science</em>');
});
});
describe('When the item has no title', () => { describe('When the item has no title', () => {
beforeEach(() => { beforeEach(() => {
publicationListElementComponent.object = mockItemWithoutMetadata; publicationListElementComponent.object = mockItemWithoutMetadata;

View File

@@ -33,7 +33,7 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
ngOnInit(): void { ngOnInit(): void {
if (hasValue(this.object)) { if (hasValue(this.object)) {
this.dso = this.object.indexableObject; this.dso = this.object.indexableObject;
this.dsoTitle = this.dsoNameService.getName(this.dso); this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso);
} }
} }

View File

@@ -29,3 +29,10 @@
ngDefaultControl ngDefaultControl
></ds-filter-input-suggestions> ></ds-filter-input-suggestions>
</div> </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>

View File

@@ -1,155 +1,155 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component'; 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 { SearchService } from '../../../../../core/shared/search/search.service';
import { import {
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH, IN_PLACE_SEARCH,
REFRESH_FILTER, SearchFilterService,
SearchFilterService REFRESH_FILTER
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { SearchFiltersComponent } from '../../search-filters.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RouterStub } from '../../../../testing/router.stub'; import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { SearchServiceStub } from '../../../../testing/search-service.stub';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub'; 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 { 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', () => { describe('SearchHierarchyFilterComponent', () => {
let comp: SearchHierarchyFilterComponent;
let fixture: ComponentFixture<SearchHierarchyFilterComponent>; let fixture: ComponentFixture<SearchHierarchyFilterComponent>;
let searchService: SearchService; let showVocabularyTreeLink: DebugElement;
let router;
const value1 = 'testvalue1'; const testSearchLink = 'test-search';
const value2 = 'test2'; const testSearchFilter = 'test-search-filter';
const value3 = 'another value3'; const VocabularyTreeViewComponent = {
const values: FacetValue[] = [ select: new EventEmitter<VocabularyEntryDetail>(),
{
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 remoteDataBuildServiceStub = { const searchService = {
aggregate(_input: Observable<RemoteData<FacetValue>>[]): Observable<RemoteData<PaginatedList<FacetValue>[]>> { getSearchLink: () => testSearchLink,
return createSuccessfulRemoteDataObject$([createPaginatedList(values)]); 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 () => { beforeEach(() => {
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], imports: [
CommonModule,
NgbModule,
TranslateModule.forRoot(),
],
declarations: [ declarations: [
SearchHierarchyFilterComponent, SearchHierarchyFilterComponent,
SearchFiltersComponent,
FilterInputSuggestionsComponent
], ],
providers: [ providers: [
{ provide: SearchService, useValue: new SearchServiceStub() }, { provide: SearchService, useValue: searchService },
{ provide: SearchFilterService, useValue: searchFilterServiceStub }, { provide: SearchFilterService, useValue: searchFilterService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildServiceStub }, { provide: RemoteDataBuildService, useValue: {} },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: router },
{ provide: NgbModal, useValue: ngbModal },
{ provide: VocabularyService, useValue: vocabularyService },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false }, { provide: IN_PLACE_SEARCH, useValue: false },
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() }, { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) } { provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false)}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(SearchHierarchyFilterComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents(); }).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); 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(); 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', () => { describe('if the vocabulary exists', () => {
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true);
const searchQuery = 'MARVEL'; beforeEach(() => {
comp.onSubmit(searchQuery); 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({ it('should show the vocabulary tree link', () => {
queryParams: Object({ [mockFilterConfig.paramName]: [...values.map((value: FacetValue) => `${value.value},equals`), `${searchQuery},query`] }), expect(showVocabularyTreeLink).toBeTruthy();
queryParamsHandling: 'merge' });
}));
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',
}));
});
});
});
}); });
}); });

View File

@@ -1,7 +1,30 @@
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { FilterType } from '../../../models/filter-type.model';
import { renderFacetFor } from '../search-filter-type-decorator'; 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 { 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'; import { addOperatorToFilterValue } from '../../../search.utils';
@Component({ @Component({
@@ -16,6 +39,23 @@ import { addOperatorToFilterValue } from '../../../search.utils';
*/ */
@renderFacetFor(FilterType.hierarchy) @renderFacetFor(FilterType.hierarchy)
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit { 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 * 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 * 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) { onSubmit(data: any) {
super.onSubmit(addOperatorToFilterValue(data, 'query')); 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;
}
}
} }

View File

@@ -186,7 +186,6 @@ import {
ImportableListItemControlComponent ImportableListItemControlComponent
} from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
import { LogInContainerComponent } from './log-in/container/log-in-container.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 { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component';
import { LogInComponent } from './log-in/log-in.component'; import { LogInComponent } from './log-in/log-in.component';
import { MissingTranslationHelper } from './translate/missing-translation.helper'; 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 { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component';
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component'; import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
import { DsSelectComponent } from './ds-select/ds-select.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 { 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 { BrowserOnlyPipe } from './utils/browser-only.pipe';
import { ThemedLoadingComponent } from './loading/themed-loading.component'; import { ThemedLoadingComponent } from './loading/themed-loading.component';
import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.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'; } from './object-list/listable-notification-object/listable-notification-object.component';
import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component'; import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component';
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.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 = [ const MODULES = [
CommonModule, CommonModule,
@@ -386,9 +385,7 @@ const ENTRY_COMPONENTS = [
MetadataRepresentationListElementComponent, MetadataRepresentationListElementComponent,
ItemMetadataRepresentationListElementComponent, ItemMetadataRepresentationListElementComponent,
LogInPasswordComponent, LogInPasswordComponent,
LogInShibbolethComponent, LogInExternalProviderComponent,
LogInOidcComponent,
LogInOrcidComponent,
CollectionDropdownComponent, CollectionDropdownComponent,
ThemedCollectionDropdownComponent, ThemedCollectionDropdownComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,

View File

@@ -11,7 +11,7 @@ describe('Angulartics2DSpace', () => {
beforeEach(() => { beforeEach(() => {
angulartics2 = { angulartics2 = {
eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}), eventTrack: observableOf({action: 'page_view', properties: {object: 'mock-object'}}),
filterDeveloperMode: () => filter(() => true) filterDeveloperMode: () => filter(() => true)
} as any; } as any;
statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null}); statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null});

View File

@@ -24,7 +24,7 @@ export class Angulartics2DSpace {
} }
private eventTrack(event) { private eventTrack(event) {
if (event.action === 'pageView') { if (event.action === 'page_view') {
this.statisticsService.trackViewEvent(event.properties.object); this.statisticsService.trackViewEvent(event.properties.object);
} else if (event.action === 'search') { } else if (event.action === 'search') {
this.statisticsService.trackSearchEvent( this.statisticsService.trackSearchEvent(

View File

@@ -20,7 +20,7 @@ export class ViewTrackerComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.angulartics2.eventTrack.next({ this.angulartics2.eventTrack.next({
action: 'pageView', action: 'page_view',
properties: {object: this.object}, properties: {object: this.object},
}); });
} }

View File

@@ -1,4 +1,7 @@
import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; import {
Angulartics2GoogleAnalytics,
Angulartics2GoogleGlobalSiteTag,
} from 'angulartics2';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { GoogleAnalyticsService } from './google-analytics.service'; import { GoogleAnalyticsService } from './google-analytics.service';
@@ -16,7 +19,7 @@ describe('GoogleAnalyticsService', () => {
const srcTestValue = 'mock-script-src'; const srcTestValue = 'mock-script-src';
let service: GoogleAnalyticsService; let service: GoogleAnalyticsService;
let googleAnalyticsSpy: Angulartics2GoogleAnalytics; let googleAnalyticsSpy: Angulartics2GoogleAnalytics;
let googleTagManagerSpy: Angulartics2GoogleTagManager; let googleTagManagerSpy: Angulartics2GoogleGlobalSiteTag;
let configSpy: ConfigurationDataService; let configSpy: ConfigurationDataService;
let klaroServiceSpy: jasmine.SpyObj<KlaroService>; let klaroServiceSpy: jasmine.SpyObj<KlaroService>;
let scriptElementMock: any; let scriptElementMock: any;
@@ -37,7 +40,7 @@ describe('GoogleAnalyticsService', () => {
googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [ googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [
'startTracking', 'startTracking',
]); ]);
googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [ googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleGlobalSiteTag', [
'startTracking', 'startTracking',
]); ]);

View File

@@ -1,7 +1,10 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; import {
Angulartics2GoogleAnalytics,
Angulartics2GoogleGlobalSiteTag,
} from 'angulartics2';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service';
@@ -19,7 +22,7 @@ export class GoogleAnalyticsService {
constructor( constructor(
private googleAnalytics: Angulartics2GoogleAnalytics, private googleAnalytics: Angulartics2GoogleAnalytics,
private googleTagManager: Angulartics2GoogleTagManager, private googleGlobalSiteTag: Angulartics2GoogleGlobalSiteTag,
private klaroService: KlaroService, private klaroService: KlaroService,
private configService: ConfigurationDataService, private configService: ConfigurationDataService,
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any,
@@ -70,7 +73,7 @@ export class GoogleAnalyticsService {
this.document.body.appendChild(libScript); this.document.body.appendChild(libScript);
// start tracking // start tracking
this.googleTagManager.startTracking(); this.googleGlobalSiteTag.startTracking();
} else { } else {
// add trackingId snippet to page // add trackingId snippet to page
const keyScript = this.document.createElement('script'); const keyScript = this.document.createElement('script');

View File

@@ -3640,6 +3640,7 @@
"search.filters.filter.submitter.label": "Search submitter", "search.filters.filter.submitter.label": "Search submitter",
"search.filters.filter.show-tree": "Browse {{ name }} tree",
"search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Journal Issue",
@@ -3836,6 +3837,8 @@
"submission.import-external.source.crossref": "CrossRef", "submission.import-external.source.crossref": "CrossRef",
"submission.import-external.source.datacite": "DataCite",
"submission.import-external.source.scielo": "SciELO", "submission.import-external.source.scielo": "SciELO",
"submission.import-external.source.scopus": "Scopus", "submission.import-external.source.scopus": "Scopus",
@@ -4512,7 +4515,7 @@
"vocabulary-treeview.tree.description.srsc": "Research Subject Categories", "vocabulary-treeview.tree.description.srsc": "Research Subject Categories",
"vocabulary-treeview.info": "Select a subject to add as search filter",
"uploader.browse": "browse", "uploader.browse": "browse",

View File

@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
import { CommunityListConfig } from './community-list-config.interface'; import { CommunityListConfig } from './community-list-config.interface';
import { HomeConfig } from './homepage-config.interface'; import { HomeConfig } from './homepage-config.interface';
import { MarkdownConfig } from './markdown-config.interface'; import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config';
interface AppConfig extends Config { interface AppConfig extends Config {
ui: UIServerConfig; ui: UIServerConfig;
@@ -44,6 +45,7 @@ interface AppConfig extends Config {
actuators: ActuatorsConfig actuators: ActuatorsConfig
info: InfoConfig; info: InfoConfig;
markdown: MarkdownConfig; markdown: MarkdownConfig;
vocabularies: FilterVocabularyConfig[];
} }
/** /**

View File

@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
import { CommunityListConfig } from './community-list-config.interface'; import { CommunityListConfig } from './community-list-config.interface';
import { HomeConfig } from './homepage-config.interface'; import { HomeConfig } from './homepage-config.interface';
import { MarkdownConfig } from './markdown-config.interface'; import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config';
export class DefaultAppConfig implements AppConfig { export class DefaultAppConfig implements AppConfig {
production = false; production = false;
@@ -385,4 +386,15 @@ export class DefaultAppConfig implements AppConfig {
enabled: false, enabled: false,
mathjax: 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
}
];
} }

View 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;
}

View File

@@ -283,4 +283,12 @@ export const environment: BuildConfig = {
enabled: false, enabled: false,
mathjax: false, mathjax: false,
}, },
vocabularies: [
{
filter: 'subject',
vocabulary: 'srsc',
enabled: true
}
]
}; };

View File

@@ -31,6 +31,7 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se
import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { AuthRequestService } from '../../app/core/auth/auth-request.service';
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service'; import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
import { BrowserInitService } from './browser-init.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'); export const REQ_KEY = makeStateKey<string>('req');
@@ -111,6 +112,10 @@ export function getRequest(transferState: TransferState): any {
provide: LocationToken, provide: LocationToken,
useFactory: locationProvider, useFactory: locationProvider,
}, },
{
provide: VocabularyTreeviewService,
useClass: VocabularyTreeviewService,
}
] ]
}) })
export class BrowserAppModule { export class BrowserAppModule {

View File

@@ -6,7 +6,11 @@ import { ServerModule, ServerTransferStateModule } from '@angular/platform-serve
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 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'; import { AppComponent } from '../../app/app.component';
@@ -63,7 +67,7 @@ export function createTranslateLoader(transferState: TransferState) {
useClass: AngularticsProviderMock useClass: AngularticsProviderMock
}, },
{ {
provide: Angulartics2GoogleTagManager, provide: Angulartics2GoogleGlobalSiteTag,
useClass: AngularticsProviderMock useClass: AngularticsProviderMock
}, },
{ {