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 }, {