diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index 81e9513433..7984fc50d1 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -143,7 +143,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { initialisePage() { this.subs.push(this.route.params.subscribe((params) => { - this.setActiveGroup(params.groupId); + if (params.groupId !== 'newGroup') { + this.setActiveGroup(params.groupId); + } })); this.canEdit$ = this.groupDataService.getActiveGroup().pipe( hasValueOperator(), @@ -225,14 +227,12 @@ export class GroupFormComponent implements OnInit, OnDestroy { { value: this.groupDescription.value } - ], + ] }, }; if (group === null) { - console.log('createNewGroup', values); this.createNewGroup(values); } else { - console.log('editGroup', group); this.editGroup(group); } } diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html index 0ac67aff75..8e2d23f8d5 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -24,10 +24,10 @@ - @@ -42,23 +42,23 @@ - - {{ePerson.id}} - {{ePerson.name}} + + {{ePerson.eperson.id}} + {{ePerson.eperson.name}}
- -
@@ -70,7 +70,7 @@
- @@ -45,7 +45,6 @@ {{messagePrefix + 'table.id' | translate}} {{messagePrefix + 'table.name' | translate}} {{messagePrefix + 'table.members' | translate}} - {{messagePrefix + 'table.edit' | translate}} @@ -53,8 +52,7 @@ {{groupDto.group.id}} {{groupDto.group.name}} - {{(getMembers(groupDto.group) | async)?.payload?.totalElements + (getSubgroups(groupDto.group) | async)?.payload?.totalElements}} - + {{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index db5b1d3e3b..305da75eeb 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -26,7 +26,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, - getAllSucceededRemoteData + getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { PageInfo } from '../../../core/shared/page-info.model'; import { hasValue } from '../../../shared/empty.util'; @@ -55,15 +55,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { currentPage: 1 }); - /** - * A list of all the current Groups within the repository or the result of the search - */ - groups$: BehaviorSubject>> = new BehaviorSubject>>({} as any); /** * A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or * as the result of the search */ groupsDto$: BehaviorSubject> = new BehaviorSubject>({} as any); + deletedGroupsIds: string[] = []; /** * An observable for the pageInfo, needed to pass to the pagination component @@ -104,30 +101,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { ngOnInit() { this.search({ query: this.currentSearchQuery }); - - this.subs.push(this.groups$.pipe( - getAllSucceededRemoteDataPayload(), - switchMap((groups: PaginatedList) => { - return observableCombineLatest(groups.page.map((group: Group) => { - return observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), - this.hasLinkedDSO(group) - ]).pipe( - map(([isAuthorized, hasLinkedDSO]: boolean[]) => { - const groupDtoModel: GroupDtoModel = new GroupDtoModel(); - groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; - groupDtoModel.group = group; - return groupDtoModel; - } - ) - ); - })).pipe(map((dtos: GroupDtoModel[]) => { - return buildPaginatedList(groups.pageInfo, dtos); - })); - })).subscribe((value: PaginatedList) => { - this.groupsDto$.next(value); - this.pageInfoState$.next(value.pageInfo); - })); } /** @@ -154,14 +127,42 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.searchSub.unsubscribe(); this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); } + this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize }).pipe( - getAllSucceededRemoteData() - ).subscribe((groupsRD: RemoteData>) => { - this.groups$.next(groupsRD); - this.pageInfoState$.next(groupsRD.payload.pageInfo); + getAllSucceededRemoteDataPayload(), + switchMap((groups: PaginatedList) => { + if (groups.page.length === 0) { + return observableOf(buildPaginatedList(groups.pageInfo, [])); + } + return observableCombineLatest(groups.page.map((group: Group) => { + if (!this.deletedGroupsIds.includes(group.id)) { + return observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), + this.hasLinkedDSO(group), + this.getSubgroups(group), + this.getMembers(group) + ]).pipe( + map(([isAuthorized, hasLinkedDSO, subgroups, members]: + [boolean, boolean, RemoteData>, RemoteData>]) => { + const groupDtoModel: GroupDtoModel = new GroupDtoModel(); + groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; + groupDtoModel.group = group; + groupDtoModel.subgroups = subgroups.payload; + groupDtoModel.epersons = members.payload; + return groupDtoModel; + } + ) + ); + } + })).pipe(map((dtos: GroupDtoModel[]) => { + return buildPaginatedList(groups.pageInfo, dtos); + })); + })).subscribe((value: PaginatedList) => { + this.groupsDto$.next(value); + this.pageInfoState$.next(value.pageInfo); }); this.subs.push(this.searchSub); } @@ -169,16 +170,17 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { /** * Delete Group */ - deleteGroup(group: Group) { - if (hasValue(group.id)) { - this.groupService.delete(group.id).pipe(getFirstCompletedRemoteData()) + deleteGroup(group: GroupDtoModel) { + if (hasValue(group.group.id)) { + this.groupService.delete(group.group.id).pipe(getFirstCompletedRemoteData()) .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name })); + this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; + this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); this.reset(); } else { this.notificationsService.error( - this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }), + this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage })); } }); @@ -201,7 +203,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { * @param group */ getMembers(group: Group): Observable>> { - return this.ePersonDataService.findAllByHref(group._links.epersons.href); + return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData()); } /** @@ -209,7 +211,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { * @param group */ getSubgroups(group: Group): Observable>> { - return this.groupService.findAllByHref(group._links.subgroups.href); + return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData()); } /** @@ -218,6 +220,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { */ hasLinkedDSO(group: Group): Observable { return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe( + getFirstSucceededRemoteData(), map((rd: RemoteData) => hasValue(rd) && hasValue(rd.payload)), catchError(() => observableOf(false)), ); @@ -233,15 +236,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.search({ query: '' }); } - /** - * Extract optional UUID from a group name => To be resolved to community or collection with link - * (Or will be resolved in backend and added to group object, tbd) //TODO - * @param groupName - */ - getOptionalComColFromName(groupName: string): string { - return this.groupService.getUUIDFromString(groupName); - } - /** * Unsub all subscriptions */ diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 7eec1c0ff9..233f15ccea 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -32,6 +32,7 @@ import { storeModuleConfig } from './app.reducer'; import { LocaleService } from './core/locale/locale.service'; import { authReducer } from './core/auth/auth.reducer'; import { provideMockStore } from '@ngrx/store/testing'; +import {GoogleAnalyticsService} from './statistics/google-analytics.service'; let comp: AppComponent; let fixture: ComponentFixture; @@ -48,38 +49,40 @@ describe('App component', () => { }); } + const defaultTestBedConf = { + imports: [ + CommonModule, + StoreModule.forRoot(authReducer, storeModuleConfig), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [AppComponent], // declare the test component + providers: [ + { provide: NativeWindowService, useValue: new NativeWindowRef() }, + { provide: MetadataService, useValue: new MetadataServiceMock() }, + { provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() }, + { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: Router, useValue: new RouterMock() }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + { provide: MenuService, useValue: menuService }, + { provide: CSSVariableService, useClass: CSSVariableServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: LocaleService, useValue: getMockLocaleService() }, + provideMockStore({ initialState }), + AppComponent, + RouteService + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }; + // waitForAsync beforeEach beforeEach(waitForAsync(() => { - return TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot(authReducer, storeModuleConfig), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - declarations: [AppComponent], // declare the test component - providers: [ - { provide: NativeWindowService, useValue: new NativeWindowRef() }, - { provide: MetadataService, useValue: new MetadataServiceMock() }, - { provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() }, - { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, - { provide: AuthService, useValue: new AuthServiceMock() }, - { provide: Router, useValue: new RouterMock() }, - { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, - { provide: MenuService, useValue: menuService }, - { provide: CSSVariableService, useClass: CSSVariableServiceStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: LocaleService, useValue: getMockLocaleService() }, - provideMockStore({ initialState }), - AppComponent, - RouteService - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); + return TestBed.configureTestingModule(defaultTestBedConf); })); // synchronous beforeEach @@ -113,4 +116,31 @@ describe('App component', () => { }); }); + + describe('when GoogleAnalyticsService is provided', () => { + let googleAnalyticsSpy; + + beforeEach(() => { + // NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset + TestBed.resetTestingModule(); + TestBed.configureTestingModule(defaultTestBedConf); + googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ + 'addTrackingIdToPage', + ]); + TestBed.overrideProvider(GoogleAnalyticsService, {useValue: googleAnalyticsSpy}); + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(comp).toBeTruthy(); + }); + + describe('the constructor', () => { + it('should call googleAnalyticsService.addTrackingIdToPage()', () => { + expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 10cda90755..1ef5c868a6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -33,6 +33,7 @@ import { models } from './core/core.module'; import { LocaleService } from './core/locale/locale.service'; import { hasValue } from './shared/empty.util'; import { KlaroService } from './shared/cookies/klaro.service'; +import {GoogleAnalyticsService} from './statistics/google-analytics.service'; @Component({ selector: 'ds-app', @@ -70,7 +71,8 @@ export class AppComponent implements OnInit, AfterViewInit { private menuService: MenuService, private windowService: HostWindowService, private localeService: LocaleService, - @Optional() private cookiesService: KlaroService + @Optional() private cookiesService: KlaroService, + @Optional() private googleAnalyticsService: GoogleAnalyticsService, ) { /* Use models object so all decorators are actually called */ @@ -84,7 +86,10 @@ export class AppComponent implements OnInit, AfterViewInit { // set the current language code this.localeService.setCurrentLanguageCode(); - angulartics2GoogleAnalytics.startTracking(); + // analytics + if (hasValue(googleAnalyticsService)) { + googleAnalyticsService.addTrackingIdToPage(); + } angulartics2DSpace.startTracking(); metadata.listenForRouteChange(); diff --git a/src/app/core/data/relationship-type.service.spec.ts b/src/app/core/data/relationship-type.service.spec.ts index 14669e68fa..910b7662dc 100644 --- a/src/app/core/data/relationship-type.service.spec.ts +++ b/src/app/core/data/relationship-type.service.spec.ts @@ -9,10 +9,10 @@ import { import { ObjectCacheService } from '../cache/object-cache.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { PageInfo } from '../shared/page-info.model'; -import { buildPaginatedList } from './paginated-list.model'; import { RelationshipTypeService } from './relationship-type.service'; import { RequestService } from './request.service'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { hasValueOperator } from '../../shared/empty.util'; describe('RelationshipTypeService', () => { let service: RelationshipTypeService; @@ -62,7 +62,7 @@ describe('RelationshipTypeService', () => { rightType: createSuccessfulRemoteDataObject$(orgUnitType) }); - buildList = createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [relationshipType1, relationshipType2])); + buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2])); rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList)); objectCache = Object.assign({ /* tslint:disable:no-empty */ @@ -100,9 +100,10 @@ describe('RelationshipTypeService', () => { describe('getRelationshipTypeByLabelAndTypes', () => { it('should return the type filtered by label and type strings', (done) => { - const expected = service.getRelationshipTypeByLabelAndTypes(relationshipType1.leftwardType, publicationTypeString, personTypeString); - expected.subscribe((e) => { - expect(e).toBe(relationshipType1); + service.getRelationshipTypeByLabelAndTypes(relationshipType1.leftwardType, publicationTypeString, personTypeString).pipe( + hasValueOperator() + ).subscribe((e) => { + expect(e.id).toEqual(relationshipType1.id); done(); }); }); diff --git a/src/app/core/data/relationship-type.service.ts b/src/app/core/data/relationship-type.service.ts index 5df11011a3..4dac044090 100644 --- a/src/app/core/data/relationship-type.service.ts +++ b/src/app/core/data/relationship-type.service.ts @@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, find, map, mergeMap, switchMap } from 'rxjs/operators'; +import { map, mergeMap, switchMap, toArray } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; -import { isNotUndefined } from '../../shared/empty.util'; +import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; @@ -15,7 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type'; -import { getFirstSucceededRemoteData } from '../shared/operators'; +import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../shared/operators'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { ItemDataService } from './item-data.service'; @@ -23,6 +23,15 @@ import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; +/** + * Check if one side of a RelationshipType is the ItemType with the given label + * + * @param typeRd the RemoteData for an ItemType + * @param label the label to check. e.g. Author + */ +const checkSide = (typeRd: RemoteData, label: string): boolean => + typeRd.hasSucceeded && typeRd.payload.label === label; + /** * The service handling all relationship type requests */ @@ -45,36 +54,70 @@ export class RelationshipTypeService extends DataService { } /** - * Get the RelationshipType for a relationship type by label - * @param label + * Find a RelationshipType object by its label, and the types of the items on either side. + * + * TODO this should be implemented as a rest endpoint, we shouldn't have logic on the client that + * requires using a huge page-size in order to process "everything". + * + * @param relationshipTypeLabel The name of the relationType we're looking for + * e.g. isAuthorOfPublication + * @param firstItemType The type of one of the sides of the relationship e.g. Publication + * @param secondItemType The type of the other side of the relationship e.g. Author */ - getRelationshipTypeByLabelAndTypes(label: string, firstType: string, secondType: string): Observable { + getRelationshipTypeByLabelAndTypes(relationshipTypeLabel: string, firstItemType: string, secondItemType: string): Observable { + // Retrieve all relationship types from the server in a single page return this.findAll({ currentPage: 1, elementsPerPage: 9999 }, true, true, followLink('leftType'), followLink('rightType')) .pipe( getFirstSucceededRemoteData(), - /* Flatten the page so we can treat it like an observable */ + // Emit each type in the page array separately switchMap((typeListRD: RemoteData>) => typeListRD.payload.page), - mergeMap((type: RelationshipType) => { - if (type.leftwardType === label) { - return this.checkType(type, firstType, secondType); - } else if (type.rightwardType === label) { - return this.checkType(type, secondType, firstType); + // Check each type individually, to see if it matches the provided types + mergeMap((relationshipType: RelationshipType) => { + if (relationshipType.leftwardType === relationshipTypeLabel) { + return this.checkType(relationshipType, firstItemType, secondItemType); + } else if (relationshipType.rightwardType === relationshipTypeLabel) { + return this.checkType(relationshipType, secondItemType, firstItemType); } else { - return []; + return [null]; } }), + // Wait for all types to be checked and emit once, with the results combined back into an + // array + toArray(), + // Look for a match in the array and emit it if found, or null if one isn't found + map((types: RelationshipType[]) => { + const match = types.find((type: RelationshipType) => hasValue(type)); + if (hasValue(match)) { + return match; + } else { + return null; + } + }) ); } - // Check if relationship type matches the given types - // returns a void observable if there's not match - // returns an observable that emits the relationship type when there is a match - private checkType(type: RelationshipType, firstType: string, secondType: string): Observable { - const entityTypes = observableCombineLatest([type.leftType.pipe(getFirstSucceededRemoteData()), type.rightType.pipe(getFirstSucceededRemoteData())]); - return entityTypes.pipe( - find(([leftTypeRD, rightTypeRD]: [RemoteData, RemoteData]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType), - filter((types) => isNotUndefined(types)), - map(() => type) + /** + * Check if the given RelationshipType has the given itemTypes on its left and right sides. + * Returns an observable of the given RelationshipType if it matches, null if it doesn't + * + * @param type The RelationshipType to check + * @param leftItemType The item type that should be on the left side + * @param rightItemType The item type that should be on the right side + * @private + */ + private checkType(type: RelationshipType, leftItemType: string, rightItemType: string): Observable { + return observableCombineLatest([ + type.leftType.pipe(getFirstCompletedRemoteData()), + type.rightType.pipe(getFirstCompletedRemoteData()) + ]).pipe( + map(([leftTypeRD, rightTypeRD]: [RemoteData, RemoteData]) => { + if (checkSide(leftTypeRD, leftItemType) && checkSide(rightTypeRD, rightItemType) + ) { + return type; + } else { + return null; + } + }) ); } } diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 9d1be80366..79df246833 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -63,10 +63,10 @@ export class EPersonDataService extends DataService { * @param query Query of search * @param options Options of search request */ - public searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable>> { + public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean): Observable>> { switch (scope) { case 'metadata': - return this.getEpeopleByMetadata(query.trim(), options); + return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable); case 'email': return this.getEPersonByEmail(query.trim()).pipe( map((rd: RemoteData) => { @@ -100,7 +100,7 @@ export class EPersonDataService extends DataService { }) ); default: - return this.getEpeopleByMetadata(query.trim(), options); + return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable); } } diff --git a/src/app/core/eperson/models/eperson-dto.model.ts b/src/app/core/eperson/models/eperson-dto.model.ts index f491f6f8be..0e79902196 100644 --- a/src/app/core/eperson/models/eperson-dto.model.ts +++ b/src/app/core/eperson/models/eperson-dto.model.ts @@ -13,5 +13,9 @@ export class EpersonDtoModel { * Whether or not the linked EPerson is able to be deleted */ public ableToDelete: boolean; + /** + * Whether or not this EPerson is member of group on page it is being used on + */ + public memberOfGroup: boolean; } diff --git a/src/app/core/eperson/models/group-dto.model.ts b/src/app/core/eperson/models/group-dto.model.ts index db167dc6b2..47a70cf326 100644 --- a/src/app/core/eperson/models/group-dto.model.ts +++ b/src/app/core/eperson/models/group-dto.model.ts @@ -1,7 +1,9 @@ +import { PaginatedList } from '../../data/paginated-list.model'; +import { EPerson } from './eperson.model'; import { Group } from './group.model'; /** - * This class serves as a Data Transfer Model that contains the Group and whether or not it's able to be deleted + * This class serves as a Data Transfer Model that contains the Group, whether or not it's able to be deleted and its members */ export class GroupDtoModel { @@ -9,9 +11,20 @@ export class GroupDtoModel { * The Group linked to this object */ public group: Group; + /** * Whether or not the linked Group is able to be deleted */ public ableToDelete: boolean; + /** + * List of subgroups of this group + */ + public subgroups: PaginatedList; + + /** + * List of members of this group + */ + public epersons: PaginatedList; + } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 9e13627116..f147cc53a6 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; import { link, typedObject } from '../../cache/builders/build-decorators'; import { PaginatedList } from '../../data/paginated-list.model'; @@ -10,12 +10,20 @@ import { HALLink } from '../../shared/hal-link.model'; import { EPerson } from './eperson.model'; import { EPERSON } from './eperson.resource-type'; import { GROUP } from './group.resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; @typedObject @inheritSerialization(DSpaceObject) export class Group extends DSpaceObject { static type = GROUP; + /** + * A string representing the unique name of this Group + */ + @excludeFromEquals + @autoserializeAs('name') + protected _name: string; + /** * A string representing the unique handle of this Group */ diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index b88adfe861..9d1fba4f86 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -29,7 +29,7 @@ export class DSpaceObject extends ListableObject implements CacheableObject { @excludeFromEquals @deserializeAs('name') - private _name: string; + protected _name: string; /** * The human-readable identifier of this DSpaceObject diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 4901c7a032..339f6f278d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -23,7 +23,7 @@ - { let pSearchOptions; let externalSourceService; let lookupRelationService; + let rdbService; let submissionId; let submissionService; let submissionObjectDataService; @@ -80,18 +82,23 @@ describe('DsDynamicLookupRelationModalComponent', () => { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true, - searchConfiguration: 'personConfig' + searchConfiguration: 'personConfig', + externalSources: ['orcidV2', 'sherpaPublisher'] }); nameVariant = 'Doe, J.'; metadataField = 'dc.contributor.author'; pSearchOptions = new PaginatedSearchOptions({}); externalSourceService = jasmine.createSpyObj('externalSourceService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources)) + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources)), + findById: createSuccessfulRemoteDataObject$(externalSources[0]) }); lookupRelationService = jasmine.createSpyObj('lookupRelationService', { getTotalLocalResults: observableOf(totalLocal), getTotalExternalResults: observableOf(totalExternal) }); + rdbService = jasmine.createSpyObj('rdbService', { + aggregate: createSuccessfulRemoteDataObject$(externalSources) + }); submissionService = jasmine.createSpyObj('SubmissionService', { dispatchSave: jasmine.createSpy('dispatchSave') }); @@ -121,6 +128,7 @@ describe('DsDynamicLookupRelationModalComponent', () => { provide: RelationshipService, useValue: { getNameVariant: () => observableOf(nameVariant) } }, { provide: RelationshipTypeService, useValue: {} }, + { provide: RemoteDataBuildService, useValue: rdbService }, { provide: SubmissionService, useValue: submissionService }, { provide: SubmissionObjectDataService, useValue: submissionObjectDataService }, { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index face405026..474a3376c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { hasValue } from '../../../../empty.util'; +import { hasValue, isNotEmpty } from '../../../../empty.util'; import { map, skip, switchMap, take } from 'rxjs/operators'; import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; @@ -11,7 +11,7 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob import { RelationshipOptions } from '../../models/relationship-options.model'; import { SearchResult } from '../../../../search/search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; @@ -19,11 +19,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../../../../app.reducer'; import { Context } from '../../../../../core/shared/context.model'; import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { ExternalSource } from '../../../../../core/shared/external-source.model'; import { ExternalSourceService } from '../../../../../core/data/external-source.service'; import { Router } from '@angular/router'; +import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { followLink } from '../../../../utils/follow-link-config.model'; import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model'; import { Collection } from '../../../../../core/shared/collection.model'; @@ -106,7 +105,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy /** * A list of the available external sources configured for this relationship */ - externalSourcesRD$: Observable>>; + externalSourcesRD$: Observable; /** * The total amount of internal items for the current options @@ -131,6 +130,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy private externalSourceService: ExternalSourceService, private lookupRelationService: LookupRelationService, private searchConfigService: SearchConfigurationService, + private rdbService: RemoteDataBuildService, private submissionService: SubmissionService, private submissionObjectService: SubmissionObjectDataService, private zone: NgZone, @@ -155,7 +155,13 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy this.context = Context.EntitySearchModal; } - this.externalSourcesRD$ = this.externalSourceService.findAll(); + if (isNotEmpty(this.relationshipOptions.externalSources)) { + this.externalSourcesRD$ = this.rdbService.aggregate( + this.relationshipOptions.externalSources.map((source) => this.externalSourceService.findById(source)) + ).pipe( + getAllSucceededRemoteDataPayload() + ); + } this.setTotals(); } @@ -256,16 +262,13 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ); const externalSourcesAndOptions$ = observableCombineLatest( - this.externalSourcesRD$.pipe( - getAllSucceededRemoteData(), - getRemoteDataPayload() - ), + this.externalSourcesRD$, this.searchConfigService.paginatedSearchOptions ); this.totalExternal$ = externalSourcesAndOptions$.pipe( switchMap(([sources, options]) => - observableCombineLatest(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) + observableCombineLatest(...sources.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) ); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts index 215049e408..432ac44859 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -20,6 +20,9 @@ import { SubmissionObjectDataService } from '../../../../../core/submission/subm import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; import { RequestService } from '../../../../../core/data/request.service'; +import { NotificationsService } from '../../../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; describe('RelationshipEffects', () => { let relationEffects: RelationshipEffects; @@ -45,6 +48,9 @@ describe('RelationshipEffects', () => { let relationship; let mockRelationshipService; let mockRelationshipTypeService; + let notificationsService; + let translateService; + let selectableListService; let testScheduler: TestScheduler; function init() { @@ -95,6 +101,14 @@ describe('RelationshipEffects', () => { getRelationshipTypeByLabelAndTypes: () => observableOf(relationshipType) }; + notificationsService = jasmine.createSpyObj('notificationsService', ['error']); + translateService = jasmine.createSpyObj('translateService', { + instant: 'translated-message' + }); + selectableListService = jasmine.createSpyObj('selectableListService', { + findSelectedByCondition: observableOf({}), + deselectSingle: {} + }); } beforeEach(waitForAsync(() => { @@ -114,6 +128,9 @@ describe('RelationshipEffects', () => { { provide: Store, useValue: jasmine.createSpyObj('store', ['dispatch']) }, { provide: ObjectCacheService, useValue: {} }, { provide: RequestService, useValue: {} }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: TranslateService, useValue: translateService }, + { provide: SelectableListService, useValue: selectableListService }, ], }); })); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 86557b806e..e68152f74d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -15,7 +15,7 @@ import { UpdateRelationshipNameVariantAction } from './relationship.actions'; import { Item } from '../../../../../core/shared/item.model'; -import { hasNoValue, hasValue } from '../../../../empty.util'; +import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; @@ -30,6 +30,9 @@ import { ServerSyncBufferActionTypes } from '../../../../../core/cache/server-sy import { JsonPatchOperationsActionTypes } from '../../../../../core/json-patch/json-patch-operations.actions'; import { followLink } from '../../../../utils/follow-link-config.model'; import { RemoteData } from '../../../../../core/data/remote-data'; +import { NotificationsService } from '../../../../notifications/notifications.service'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { TranslateService } from '@ngx-translate/core'; const DEBOUNCE_TIME = 500; @@ -152,7 +155,10 @@ export class RelationshipEffects { private submissionObjectService: SubmissionObjectDataService, private store: Store, private objectCache: ObjectCacheService, - private requestService: RequestService + private requestService: RequestService, + private notificationsService: NotificationsService, + private translateService: TranslateService, + private selectableListService: SelectableListService, ) { } @@ -166,6 +172,9 @@ export class RelationshipEffects { return this.relationshipTypeService.getRelationshipTypeByLabelAndTypes(relationshipType, type1, type2) .pipe( mergeMap((type: RelationshipType) => { + if (type === null) { + return [null]; + } else { const isSwitched = type.rightwardType === relationshipType; if (isSwitched) { return this.relationshipService.addRelationship(type.id, item2, item1, nameVariant, undefined); @@ -173,9 +182,28 @@ export class RelationshipEffects { return this.relationshipService.addRelationship(type.id, item1, item2, undefined, nameVariant); } } - ), + }), take(1), - switchMap(() => this.refreshWorkspaceItemInCache(submissionId)), + switchMap((rd: RemoteData) => { + if (hasNoValue(rd) || rd.hasFailed) { + // An error occurred, deselect the object from the selectable list and display an error notification + const listId = `list-${submissionId}-${relationshipType}`; + this.selectableListService.findSelectedByCondition(listId, (object: any) => hasValue(object.indexableObject) && object.indexableObject.uuid === item2.uuid).pipe( + take(1), + hasValueOperator() + ).subscribe((selected) => { + this.selectableListService.deselectSingle(listId, selected); + }); + let errorContent; + if (hasNoValue(rd)) { + errorContent = this.translateService.instant('relationships.add.error.relationship-type.content', { type: relationshipType }); + } else { + errorContent = this.translateService.instant('relationships.add.error.server.content'); + } + this.notificationsService.error(this.translateService.instant('relationships.add.error.title'), errorContent); + } + return this.refreshWorkspaceItemInCache(submissionId); + }), ).subscribe((submissionObject: SubmissionObject) => this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false))); } diff --git a/src/app/shared/form/builder/models/relationship-options.model.ts b/src/app/shared/form/builder/models/relationship-options.model.ts index 4568aa69ec..eaa62961a2 100644 --- a/src/app/shared/form/builder/models/relationship-options.model.ts +++ b/src/app/shared/form/builder/models/relationship-options.model.ts @@ -8,6 +8,7 @@ export class RelationshipOptions { filter: string; searchConfiguration: string; nameVariants: string; + externalSources: string[]; get metadataField() { return RELATION_METADATA_PREFIX + this.relationshipType; diff --git a/src/app/shared/object-list/selectable-list/selectable-list.service.ts b/src/app/shared/object-list/selectable-list/selectable-list.service.ts index 5a86c0b65d..91ecdf1a1a 100644 --- a/src/app/shared/object-list/selectable-list/selectable-list.service.ts +++ b/src/app/shared/object-list/selectable-list/selectable-list.service.ts @@ -91,4 +91,15 @@ export class SelectableListService { distinctUntilChanged() ); } + + /** + * Find a selected object by a custom condition + * @param id The ID of the selectable list to search in + * @param condition The condition that the required object has to match + */ + findSelectedByCondition(id: string, condition: (object: ListableObject) => boolean): Observable { + return this.getSelectableList(id).pipe( + map((state: SelectableListState) => (hasValue(state) && isNotEmpty(state.selection)) ? state.selection.find((selected) => condition(selected)) : undefined) + ); + } } diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts new file mode 100644 index 0000000000..5a62b02334 --- /dev/null +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -0,0 +1,128 @@ +import { GoogleAnalyticsService } from './google-analytics.service'; +import {Angulartics2GoogleAnalytics} from 'angulartics2/ga'; +import {ConfigurationDataService} from '../core/data/configuration-data.service'; +import {createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../shared/remote-data.utils'; +import {ConfigurationProperty} from '../core/shared/configuration-property.model'; + +describe('GoogleAnalyticsService', () => { + const trackingIdProp = 'google.analytics.key'; + const trackingIdTestValue = 'mock-tracking-id'; + const innerHTMLTestValue = 'mock-script-inner-html'; + let service: GoogleAnalyticsService; + let angularticsSpy: Angulartics2GoogleAnalytics; + let configSpy: ConfigurationDataService; + let scriptElementMock: any; + let innerHTMLSpy: any; + let bodyElementSpy: HTMLBodyElement; + let documentSpy: Document; + + const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: trackingIdProp, + values: values, + }), + }); + + beforeEach(() => { + angularticsSpy = jasmine.createSpyObj('angulartics2GoogleAnalytics', [ + 'startTracking', + ]); + + configSpy = createConfigSuccessSpy(trackingIdTestValue); + + scriptElementMock = { + set innerHTML(newVal) { /* noop */ }, + get innerHTML() { return innerHTMLTestValue; } + }; + + innerHTMLSpy = spyOnProperty(scriptElementMock, 'innerHTML', 'set'); + + bodyElementSpy = jasmine.createSpyObj('body', { + appendChild: scriptElementMock, + }); + + documentSpy = jasmine.createSpyObj('document', { + createElement: scriptElementMock, + }, { + body: bodyElementSpy, + }); + + service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('addTrackingIdToPage()', () => { + it(`should request the ${trackingIdProp} property`, () => { + service.addTrackingIdToPage(); + expect(configSpy.findByPropertyName).toHaveBeenCalledTimes(1); + expect(configSpy.findByPropertyName).toHaveBeenCalledWith(trackingIdProp); + }); + + describe('when the request fails', () => { + beforeEach(() => { + configSpy = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createFailedRemoteDataObject$(), + }); + + service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy); + }); + + it('should NOT add a script to the body', () => { + service.addTrackingIdToPage(); + expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(0); + }); + + it('should NOT start tracking', () => { + service.addTrackingIdToPage(); + expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + }); + }); + + describe('when the request succeeds', () => { + describe('when the tracking id is empty', () => { + beforeEach(() => { + configSpy = createConfigSuccessSpy(); + service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy); + }); + + it('should NOT add a script to the body', () => { + service.addTrackingIdToPage(); + expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(0); + }); + + it('should NOT start tracking', () => { + service.addTrackingIdToPage(); + expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + }); + }); + + describe('when the tracking id is non-empty', () => { + it('should create a script tag whose innerHTML contains the tracking id', () => { + service.addTrackingIdToPage(); + expect(documentSpy.createElement).toHaveBeenCalledTimes(1); + expect(documentSpy.createElement).toHaveBeenCalledWith('script'); + + // sanity check + expect(documentSpy.createElement('script')).toBe(scriptElementMock); + + expect(innerHTMLSpy).toHaveBeenCalledTimes(1); + expect(innerHTMLSpy.calls.argsFor(0)[0]).toContain(trackingIdTestValue); + }); + + it('should add a script to the body', () => { + service.addTrackingIdToPage(); + expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(1); + }); + + it('should start tracking', () => { + service.addTrackingIdToPage(); + expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(1); + }); + }); + }); + }); +}); diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts new file mode 100644 index 0000000000..ce4073bea5 --- /dev/null +++ b/src/app/statistics/google-analytics.service.ts @@ -0,0 +1,52 @@ +import {Inject, Injectable} from '@angular/core'; +import {Angulartics2GoogleAnalytics} from 'angulartics2/ga'; +import {ConfigurationDataService} from '../core/data/configuration-data.service'; +import {getFirstCompletedRemoteData} from '../core/shared/operators'; +import {isEmpty} from '../shared/empty.util'; +import {DOCUMENT} from '@angular/common'; + +/** + * Set up Google Analytics on the client side. + * See: {@link addTrackingIdToPage}. + */ +@Injectable() +export class GoogleAnalyticsService { + + constructor( + private angulartics: Angulartics2GoogleAnalytics, + private configService: ConfigurationDataService, + @Inject(DOCUMENT) private document: any, + ) { } + + /** + * Call this method once when Angular initializes on the client side. + * It requests a Google Analytics tracking id from the rest backend + * (property: google.analytics.key), adds the tracking snippet to the + * page and starts tracking. + */ + addTrackingIdToPage(): void { + this.configService.findByPropertyName('google.analytics.key').pipe( + getFirstCompletedRemoteData(), + ).subscribe((remoteData) => { + // make sure we got a success response from the backend + if (!remoteData.hasSucceeded) { return; } + + const trackingId = remoteData.payload.values[0]; + + // make sure we received a tracking id + if (isEmpty(trackingId)) { return; } + + // add trackingId snippet to page + const keyScript = this.document.createElement('script'); + keyScript.innerHTML = `(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', '${trackingId}', 'auto');`; + this.document.body.appendChild(keyScript); + + // start tracking + this.angulartics.startTracking(); + }); + } +} diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 951ddae4ac..32c11d6673 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -32,6 +32,8 @@ import { SubmissionImportExternalSearchbarComponent } from './import-external/im import { SubmissionImportExternalPreviewComponent } from './import-external/import-external-preview/submission-import-external-preview.component'; import { SubmissionImportExternalCollectionComponent } from './import-external/import-external-collection/submission-import-external-collection.component'; import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; +import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; +import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; @NgModule({ imports: [ @@ -39,7 +41,9 @@ import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/subm CoreModule.forRoot(), SharedModule, StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(submissionEffects) + EffectsModule.forFeature(submissionEffects), + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents(), ], declarations: [ SubmissionSectionUploadAccessConditionsComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ade2719e47..a8d7821080 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -308,8 +308,6 @@ "admin.access-control.groups.table.members": "Members", - "admin.access-control.groups.table.comcol": "Community / Collection", - "admin.access-control.groups.table.edit": "Edit", "admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"", @@ -404,6 +402,8 @@ "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", + "admin.access-control.groups.form.subgroups-list.notification.failure": "Something went wrong: \"{{cause}}\"", + "admin.access-control.groups.form.subgroups-list.head": "Groups", "admin.access-control.groups.form.subgroups-list.search.head": "Add Subgroup", @@ -2663,6 +2663,12 @@ + "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", + + "relationships.add.error.server.content": "The server returned an error", + + "relationships.add.error.title": "Unable to add relationship", + "relationships.isAuthorOf": "Authors", "relationships.isAuthorOf.Person": "Authors (persons)", diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index 0bc3d5eec4..5c6e56babb 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -23,7 +23,6 @@ export interface GlobalConfig extends Config { notifications: INotificationBoardOptions; submission: SubmissionConfig; universal: UniversalConfig; - gaTrackingId: string; logDirectory: string; debug: boolean; defaultLanguage: string; diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 1316b51f1a..4a87be38d3 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -132,8 +132,6 @@ export const environment: GlobalConfig = { async: true, time: false }, - // Google Analytics tracking id - gaTrackingId: '', // Log directory logDirectory: '.', // NOTE: will log all redux actions and transfers in console diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index b220c46083..ef3eb86cc2 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -110,8 +110,6 @@ export const environment: Partial = { async: true, time: false }, - // Google Analytics tracking id - gaTrackingId: '', // Log directory logDirectory: '.', // NOTE: will log all redux actions and transfers in console diff --git a/src/main.browser.ts b/src/main.browser.ts index 5149014d88..1f399b858a 100644 --- a/src/main.browser.ts +++ b/src/main.browser.ts @@ -6,7 +6,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { bootloader } from '@angularclass/bootloader'; import { load as loadWebFont } from 'webfontloader'; -import { hasValue, isNotEmpty } from './app/shared/empty.util'; +import { hasValue } from './app/shared/empty.util'; import { BrowserAppModule } from './modules/app/browser-app.module'; @@ -25,25 +25,9 @@ export function main() { } }); - addGoogleAnalytics(); - return platformBrowserDynamic().bootstrapModule(BrowserAppModule, {preserveWhitespaces:true}); } -function addGoogleAnalytics() { - // Add google analytics if key is present in config - const trackingId = environment.gaTrackingId; - if (isNotEmpty(trackingId)) { - const keyScript = document.createElement('script'); - keyScript.innerHTML = `(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');` - + 'ga(\'create\', \'' + environment.gaTrackingId + '\', \'auto\');'; - document.body.appendChild(keyScript); - } -} - // support async tag or hmr if (hasValue(environment.universal) && environment.universal.preboot === false) { bootloader(main); diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 3aa6bf244b..e0bd7b5ca1 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -30,6 +30,7 @@ import { LocationToken } from '../../app/core/services/browser-hard-redirect.service'; import { LocaleService } from '../../app/core/locale/locale.service'; +import {GoogleAnalyticsService} from '../../app/statistics/google-analytics.service'; export const REQ_KEY = makeStateKey('req'); @@ -99,6 +100,10 @@ export function getRequest(transferState: TransferState): any { provide: HardRedirectService, useClass: BrowserHardRedirectService, }, + { + provide: GoogleAnalyticsService, + useClass: GoogleAnalyticsService, + }, { provide: LocationToken, useFactory: locationProvider,