diff --git a/config/config.example.yml b/config/config.example.yml
index 9abf167b90..c548d6944a 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -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
diff --git a/package.json b/package.json
index 387e094a67..f6ab1274e6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts
index 09487a7eaa..a7a7cb5be4 100644
--- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts
+++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.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);
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts
index 584b28ba1e..4302d126ea 100644
--- a/src/app/access-control/group-registry/group-form/group-form.component.ts
+++ b/src/app/access-control/group-registry/group-form/group-form.component.ts
@@ -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
}];
}
diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts
index e55d0c0ff9..672879f436 100644
--- a/src/app/core/auth/auth.interceptor.ts
+++ b/src/app/core/auth/auth.interceptor.ts
@@ -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;
}
diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
index cf8fbd8c49..642d81bbd1 100644
--- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
@@ -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', () => {
diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
index 408d656f42..56909090c7 100644
--- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
@@ -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);
}
/**
diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html
index 6e4685a07b..36f7034f4d 100644
--- a/src/app/shared/log-in/log-in.component.html
+++ b/src/app/shared/log-in/log-in.component.html
@@ -1,6 +1,6 @@
-
+
{{"login.form.or-divider" | translate}}
diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts
index 120f3ac4fa..e6b2ba9dcf 100644
--- a/src/app/shared/log-in/log-in.component.ts
+++ b/src/app/shared/log-in/log-in.component.ts
@@ -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;
+ 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));
diff --git a/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html
new file mode 100644
index 0000000000..6046aec725
--- /dev/null
+++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html
@@ -0,0 +1,3 @@
+
diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.scss b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.scss
similarity index 100%
rename from src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.scss
rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.scss
diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts
similarity index 88%
rename from src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts
rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts
index 001f0a4959..de4f62eb9e 100644
--- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts
+++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts
@@ -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;
+ let component: LogInExternalProviderComponent;
+ let fixture: ComponentFixture;
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) {
+ constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture) {
// use injector to get services
const injector = fixture.debugElement.injector;
const store = injector.get(Store);
diff --git a/src/app/shared/log-in/methods/log-in-external-provider.component.ts b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts
similarity index 73%
rename from src/app/shared/log-in/methods/log-in-external-provider.component.ts
rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts
index 037fc40e90..f182968457 100644
--- a/src/app/shared/log-in/methods/log-in-external-provider.component.ts
+++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts
@@ -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}`;
+ }
}
diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html
deleted file mode 100644
index 7e78834305..0000000000
--- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
\ No newline at end of file
diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts
deleted file mode 100644
index 078a58dd5a..0000000000
--- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts
+++ /dev/null
@@ -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;
- 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) {
- // use injector to get services
- const injector = fixture.debugElement.injector;
- const store = injector.get(Store);
-
- // add spies
- this.navigateSpy = spyOn(store, 'dispatch');
- }
-
-}
diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts
deleted file mode 100644
index 882996b207..0000000000
--- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts
+++ /dev/null
@@ -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();
- }
-
-}
diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html
deleted file mode 100644
index 6f5453fd60..0000000000
--- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
\ No newline at end of file
diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts
deleted file mode 100644
index e0b1da3db5..0000000000
--- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts
+++ /dev/null
@@ -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();
- }
-
-}
diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html
deleted file mode 100644
index 3a3b935cfa..0000000000
--- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts
deleted file mode 100644
index 075d33d98e..0000000000
--- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts
+++ /dev/null
@@ -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;
- 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) {
- // use injector to get services
- const injector = fixture.debugElement.injector;
- const store = injector.get(Store);
-
- // add spies
- this.navigateSpy = spyOn(store, 'dispatch');
- }
-
-}
diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts
deleted file mode 100644
index dcfb3ccfc3..0000000000
--- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts
+++ /dev/null
@@ -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();
- }
-
-}
diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
index 49ca6fe3fd..eb49235641 100644
--- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
+++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
@@ -29,3 +29,10 @@
ngDefaultControl
>
+
+
+ {{'search.filters.filter.show-tree' | translate: {name: ('search.filters.filter.' + filterConfig.name + '.head' | translate | lowercase )} }}
+
diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts
index 9302e66d98..e6c74d8047 100644
--- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts
+++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts
@@ -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;
- 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 {
- return observableOf(values.map((value: FacetValue) => value.value));
- },
- getPage(_paramName: string): Observable {
- return observableOf(0);
- },
- resetPage(_filterName: string): void {
- // empty
- }
+ const testSearchLink = 'test-search';
+ const testSearchFilter = 'test-search-filter';
+ const VocabularyTreeViewComponent = {
+ select: new EventEmitter(),
};
- const remoteDataBuildServiceStub = {
- aggregate(_input: Observable>[]): Observable[]>> {
- 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(false) }
+ { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
+ { provide: REFRESH_FILTER, useValue: new BehaviorSubject(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',
+ }));
+ });
+ });
+ });
});
});
diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts
index b3349a5dd9..8504237f09 100644
--- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts
+++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts
@@ -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
+ ) {
+ super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
+ }
+
+ vocabularyExists$: Observable;
+
/**
* 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;
+ }
+ }
}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index e4abf0d907..777ad03c1d 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -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,
diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts
index 8491d8e80c..73c2419ce6 100644
--- a/src/app/statistics/angulartics/dspace-provider.spec.ts
+++ b/src/app/statistics/angulartics/dspace-provider.spec.ts
@@ -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});
diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts
index cd1aab94bd..6efa67f92a 100644
--- a/src/app/statistics/angulartics/dspace-provider.ts
+++ b/src/app/statistics/angulartics/dspace-provider.ts
@@ -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(
diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts
index 85588aeb97..d12b6c2f69 100644
--- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts
+++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts
@@ -20,7 +20,7 @@ export class ViewTrackerComponent implements OnInit {
ngOnInit(): void {
this.angulartics2.eventTrack.next({
- action: 'pageView',
+ action: 'page_view',
properties: {object: this.object},
});
}
diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts
index 9e2b1c7edf..2465e4db0e 100644
--- a/src/app/statistics/google-analytics.service.spec.ts
+++ b/src/app/statistics/google-analytics.service.spec.ts
@@ -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;
let scriptElementMock: any;
@@ -37,7 +40,7 @@ describe('GoogleAnalyticsService', () => {
googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [
'startTracking',
]);
- googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [
+ googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleGlobalSiteTag', [
'startTracking',
]);
diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts
index 9c5883d183..9d32a61093 100644
--- a/src/app/statistics/google-analytics.service.ts
+++ b/src/app/statistics/google-analytics.service.ts
@@ -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');
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index 1ef1aa793a..52a6f4e58c 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -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",
diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts
index ce9c8b3bf7..d62b9e5bcb 100644
--- a/src/config/app-config.interface.ts
+++ b/src/config/app-config.interface.ts
@@ -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[];
}
/**
diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts
index 276d2d7150..d7b3efc2ed 100644
--- a/src/config/default-app-config.ts
+++ b/src/config/default-app-config.ts
@@ -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
+ }
+ ];
}
diff --git a/src/config/filter-vocabulary-config.ts b/src/config/filter-vocabulary-config.ts
new file mode 100644
index 0000000000..54e57090c8
--- /dev/null
+++ b/src/config/filter-vocabulary-config.ts
@@ -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;
+}
diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts
index 19eec26a14..b323fa464d 100644
--- a/src/environments/environment.test.ts
+++ b/src/environments/environment.test.ts
@@ -283,4 +283,12 @@ export const environment: BuildConfig = {
enabled: false,
mathjax: false,
},
+
+ vocabularies: [
+ {
+ filter: 'subject',
+ vocabulary: 'srsc',
+ enabled: true
+ }
+ ]
};
diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts
index 6baf339003..4cdf7fbe2f 100644
--- a/src/modules/app/browser-app.module.ts
+++ b/src/modules/app/browser-app.module.ts
@@ -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('req');
@@ -111,6 +112,10 @@ export function getRequest(transferState: TransferState): any {
provide: LocationToken,
useFactory: locationProvider,
},
+ {
+ provide: VocabularyTreeviewService,
+ useClass: VocabularyTreeviewService,
+ }
]
})
export class BrowserAppModule {
diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts
index 17e394ede8..81426e7fcc 100644
--- a/src/modules/app/server-app.module.ts
+++ b/src/modules/app/server-app.module.ts
@@ -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
},
{