mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
Merge branch 'main-upstream' into issue-1833_entity-title-refactor-main
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
@@ -16,7 +17,6 @@ export const EPeopleRegistryActionTypes = {
|
||||
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit an EPerson in the EPeople registry
|
||||
*/
|
||||
@@ -37,7 +37,6 @@ export class EPeopleRegistryCancelEPersonAction implements Action {
|
||||
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
||||
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||
[paginationOptions]="config"
|
||||
|
@@ -9,7 +9,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
@@ -27,6 +26,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
|
@@ -36,12 +36,12 @@
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-loading>
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
|
@@ -8,7 +8,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
@@ -29,6 +28,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('EPersonFormComponent', () => {
|
||||
|
||||
});
|
||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: ''
|
||||
});
|
||||
|
||||
|
@@ -265,7 +265,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
if (eperson != null) {
|
||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, {
|
||||
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
@@ -297,7 +297,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
switchMap(([eperson, findListOptions]) => {
|
||||
if (eperson != null) {
|
||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||
}
|
||||
return observableOf(undefined);
|
||||
})
|
||||
@@ -554,7 +554,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
private updateGroups(options) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
||||
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule, FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -35,6 +35,7 @@ import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
@@ -87,6 +88,9 @@ describe('GroupFormComponent', () => {
|
||||
patch(group: Group, operations: Operation[]) {
|
||||
return null;
|
||||
},
|
||||
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||
return createSuccessfulRemoteDataObject$({});
|
||||
},
|
||||
cancelEditGroup(): void {
|
||||
this.activeGroup = null;
|
||||
},
|
||||
@@ -348,4 +352,46 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let deleteButton;
|
||||
|
||||
beforeEach(() => {
|
||||
component.initialisePage();
|
||||
|
||||
component.canEdit$ = observableOf(true);
|
||||
component.groupBeingEdited = {
|
||||
permanent: false
|
||||
} as Group;
|
||||
|
||||
fixture.detectChanges();
|
||||
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
||||
});
|
||||
|
||||
describe('if confirmed via modal', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
deleteButton.click();
|
||||
fixture.detectChanges();
|
||||
(document as any).querySelector('.modal-footer .confirm').click();
|
||||
}));
|
||||
|
||||
it('should call GroupDataService.delete', () => {
|
||||
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith('active-group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if canceled via modal', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
deleteButton.click();
|
||||
fixture.detectChanges();
|
||||
(document as any).querySelector('.modal-footer .cancel').click();
|
||||
}));
|
||||
|
||||
it('should not call GroupDataService.delete', () => {
|
||||
expect(groupsDataServiceStub.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -426,7 +426,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
.subscribe((rd: RemoteData<NoContent>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
||||
this.reset();
|
||||
this.onCancel();
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
||||
@@ -439,16 +439,6 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
});
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
|
@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
|
||||
activeGroup: activeGroup,
|
||||
epersonMembers: epersonMembers,
|
||||
subgroupMembers: subgroupMembers,
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||
},
|
||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
|
@@ -129,7 +129,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
this.subs.set(SubKey.MembersDTO,
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((currentPagination) => {
|
||||
return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((group: Group) => {
|
||||
if (group != null) {
|
||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
||||
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 9999
|
||||
}, false)
|
||||
|
@@ -65,7 +65,7 @@ describe('SubgroupsListComponent', () => {
|
||||
getSubgroups(): Group {
|
||||
return this.activeGroup;
|
||||
},
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return this.subgroups$.pipe(
|
||||
map((currentGroups: Group[]) => {
|
||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||
|
@@ -115,7 +115,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
this.subs.set(
|
||||
SubKey.Members,
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
switchMap((config) => this.groupDataService.findListByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
},
|
||||
@@ -139,7 +139,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
|
||||
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 9999
|
||||
})
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { Group } from '../../core/eperson/models/group.model';
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
@@ -16,7 +17,6 @@ export const GroupRegistryActionTypes = {
|
||||
CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit a Group in the Group registry
|
||||
*/
|
||||
@@ -37,7 +37,6 @@ export class GroupRegistryCancelGroupAction implements Action {
|
||||
type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
|
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
||||
[paginationOptions]="config"
|
||||
@@ -79,7 +79,7 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
@@ -31,6 +31,7 @@ import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
|
||||
describe('GroupRegistryComponent', () => {
|
||||
let component: GroupsRegistryComponent;
|
||||
@@ -68,7 +69,7 @@ describe('GroupRegistryComponent', () => {
|
||||
mockGroups = [GroupMock, GroupMock2];
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||
@@ -96,7 +97,7 @@ describe('GroupRegistryComponent', () => {
|
||||
};
|
||||
groupsDataServiceStub = {
|
||||
allGroups: mockGroups,
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||
@@ -145,7 +146,10 @@ describe('GroupRegistryComponent', () => {
|
||||
totalPages: 1,
|
||||
currentPage: 1
|
||||
}), [result]));
|
||||
}
|
||||
},
|
||||
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||
return createSuccessfulRemoteDataObject$({});
|
||||
},
|
||||
};
|
||||
dsoDataServiceStub = {
|
||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||
@@ -301,4 +305,29 @@ describe('GroupRegistryComponent', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let deleteButton;
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
|
||||
setIsAuthorized(true, true);
|
||||
|
||||
// force rerender after setup changes
|
||||
component.search({ query: '' });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
// only mockGroup[0] is deletable, so we should only get one button
|
||||
deleteButton = fixture.debugElement.query(By.css('.btn-delete')).nativeElement;
|
||||
}));
|
||||
|
||||
it('should call GroupDataService.delete', () => {
|
||||
deleteButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith(mockGroups[0].id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
of as observableOf,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
@@ -199,7 +199,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
if (rd.hasSucceeded) {
|
||||
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.group.name }),
|
||||
@@ -209,23 +208,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
||||
*/
|
||||
reset() {
|
||||
this.groupService.getBrowseEndpoint().pipe(
|
||||
take(1)
|
||||
).subscribe((href: string) => {
|
||||
this.requestService.setStaleByHrefSubstring(href);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the members (epersons embedded value of a group)
|
||||
* @param group
|
||||
*/
|
||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +221,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
* @param group
|
||||
*/
|
||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,17 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||
<label class="form-check-label" for="validateOnly">
|
||||
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||
</label>
|
||||
</div>
|
||||
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||
{{'admin.metadata-import.page.validateOnly.hint' | translate}}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<ds-file-dropzone-no-uploader
|
||||
(onFileAdded)="setFile($event)"
|
||||
@@ -8,8 +19,10 @@
|
||||
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
<div class="space-children-mr">
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -87,8 +87,9 @@ describe('MetadataImportPageComponent', () => {
|
||||
comp.setFile(fileMock);
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed', () => {
|
||||
describe('if proceed button is pressed without validate only', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
comp.validateOnly = false;
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
@@ -107,6 +108,28 @@ describe('MetadataImportPageComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed with validate only', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
comp.validateOnly = true;
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('metadata-import script is invoked with -f fileName and the mockFile and -v validate-only', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed is pressed; but script invoke fails', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
|
@@ -30,6 +30,11 @@ export class MetadataImportPageComponent {
|
||||
*/
|
||||
fileObject: File;
|
||||
|
||||
/**
|
||||
* The validate only flag
|
||||
*/
|
||||
validateOnly = true;
|
||||
|
||||
public constructor(private location: Location,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
@@ -62,6 +67,9 @@ export class MetadataImportPageComponent {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||
];
|
||||
if (this.validateOnly) {
|
||||
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||
}
|
||||
|
||||
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
@@ -17,7 +18,6 @@ export const BitstreamFormatsRegistryActionTypes = {
|
||||
DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_FORMAT')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to select a single bitstream format in the bitstream format registry
|
||||
*/
|
||||
@@ -51,7 +51,6 @@ export class BitstreamFormatsRegistryDeselectAllAction implements Action {
|
||||
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
|
@@ -25,9 +25,9 @@ import {
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('BitstreamFormatsComponent', () => {
|
||||
let comp: BitstreamFormatsComponent;
|
||||
|
@@ -5,7 +5,6 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
@@ -13,6 +12,7 @@ import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
@@ -26,7 +27,6 @@ export const MetadataRegistryActionTypes = {
|
||||
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit a metadata schema in the metadata registry
|
||||
*/
|
||||
@@ -133,7 +133,6 @@ export class MetadataRegistryDeselectAllFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
|
@@ -21,8 +21,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
@@ -52,7 +52,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
}
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
@@ -66,7 +66,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
|
@@ -128,7 +128,6 @@ export class MetadataRegistryComponent {
|
||||
* Delete all the selected metadata schemas
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
@@ -148,7 +147,6 @@ export class MetadataRegistryComponent {
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
this.forceUpdateSchemas();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -17,7 +17,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
@@ -33,7 +33,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
@@ -24,7 +24,7 @@ describe('MetadataFieldFormComponent', () => {
|
||||
prefix: 'fake'
|
||||
});
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||
@@ -43,7 +43,7 @@ describe('MetadataFieldFormComponent', () => {
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
@@ -25,9 +25,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -106,7 +106,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
}
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||
@@ -122,7 +122,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
const schemaNameParam = 'mock';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({
|
||||
|
@@ -174,15 +174,12 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
this.registryService.clearMetadataFieldRequests();
|
||||
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
this.forceUpdateFields();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -18,6 +18,8 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||
|
||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
let component: ItemAdminSearchResultGridElementComponent;
|
||||
@@ -31,6 +33,12 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const mockAccessStatusDataService = {
|
||||
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
|
||||
return createSuccessfulRemoteDataObject$(new AccessStatusObject());
|
||||
}
|
||||
};
|
||||
|
||||
const mockThemeService = getMockThemeService();
|
||||
|
||||
function init() {
|
||||
@@ -55,6 +63,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: ThemeService, useValue: mockThemeService },
|
||||
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
@@ -1,28 +1,30 @@
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
||||
<div class="space-children-mr my-1">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
</a>
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||
import { MenuSection } from '../../../shared/menu/menu.reducer';
|
||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Router } from '@angular/router';
|
||||
* Represents a non-expandable section in the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* tslint:disable:component-selector */
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-admin-sidebar-section]',
|
||||
templateUrl: './admin-sidebar-section.component.html',
|
||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<nav @slideHorizontal class="navbar navbar-dark p-0"
|
||||
<nav class="navbar navbar-dark p-0"
|
||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||
[@slideSidebar]="{
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
|
@@ -20,6 +20,8 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import createSpy = jasmine.createSpy;
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
|
||||
describe('AdminSidebarComponent', () => {
|
||||
let comp: AdminSidebarComponent;
|
||||
@@ -60,6 +62,7 @@ describe('AdminSidebarComponent', () => {
|
||||
declarations: [AdminSidebarComponent],
|
||||
providers: [
|
||||
Injector,
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
@@ -182,150 +185,4 @@ describe('AdminSidebarComponent', () => {
|
||||
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('menu', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'addSection');
|
||||
});
|
||||
|
||||
describe('for regular user', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
|
||||
return observableOf(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should not show site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'workflow', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not show edit_community', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: false,
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should not show edit_collection', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'access_control', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: false,
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('for site admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.AdministratorOf);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should contain site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'workflow', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for community admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show edit_community', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for collection admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show edit_collection', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for group admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.CanManageGroups);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'access_control', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,27 +1,15 @@
|
||||
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, first, map, take, distinctUntilChanged, withLatestFrom } from 'rxjs/operators';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import { ExportMetadataSelectorComponent } from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
||||
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
||||
import { slideSidebar } from '../../shared/animations/slide';
|
||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
/**
|
||||
* Component representing the admin sidebar
|
||||
@@ -30,7 +18,7 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
selector: 'ds-admin-sidebar',
|
||||
templateUrl: './admin-sidebar.component.html',
|
||||
styleUrls: ['./admin-sidebar.component.scss'],
|
||||
animations: [slideHorizontal, slideSidebar]
|
||||
animations: [slideSidebar]
|
||||
})
|
||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
/**
|
||||
@@ -63,16 +51,16 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
|
||||
inFocus$: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
constructor(
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
public route: ActivatedRoute
|
||||
public route: ActivatedRoute,
|
||||
protected themeService: ThemeService
|
||||
) {
|
||||
super(menuService, injector, authorizationService, route);
|
||||
super(menuService, injector, authorizationService, route, themeService);
|
||||
this.inFocus$ = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
@@ -80,7 +68,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
* Set and calculate all initial values of the instance variables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.createMenu();
|
||||
super.ngOnInit();
|
||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||
this.authService.isAuthenticated()
|
||||
@@ -115,501 +102,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for this menu
|
||||
*/
|
||||
createMenu() {
|
||||
this.createMainMenuSections();
|
||||
this.createSiteAdministratorMenuSections();
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
this.createAccessControlMenuSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the main menu sections.
|
||||
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
||||
*/
|
||||
createMainMenuSections() {
|
||||
combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
||||
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
||||
const menuList = [
|
||||
/* News */
|
||||
{
|
||||
id: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus',
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: 'new_community',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_process',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'new_item_version',
|
||||
// parentID: 'new',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.new_item_version',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
|
||||
/* Edit */
|
||||
{
|
||||
id: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
id: 'edit_community',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
||||
/* Statistics */
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'statistics_task',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.statistics_task',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'chart-bar',
|
||||
// index: 8
|
||||
// },
|
||||
|
||||
/* Control Panel */
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'control_panel',
|
||||
// active: false,
|
||||
// visible: isSiteAdmin,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.control_panel',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'cogs',
|
||||
// index: 9
|
||||
// },
|
||||
|
||||
/* Processes */
|
||||
{
|
||||
id: 'processes',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.processes',
|
||||
link: '/processes'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'terminal',
|
||||
index: 10
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the export scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createExportMenuSections() {
|
||||
const menuList = [
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-export',
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_community',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_community',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_collection',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_collection',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_item',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_item',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
||||
).pipe(
|
||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
|
||||
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.menuService.addSection(this.menuID, {
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the import scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createImportMenuSections() {
|
||||
const menuList = [
|
||||
/* Import */
|
||||
{
|
||||
id: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-import',
|
||||
index: 2
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'import_batch',
|
||||
// parentID: 'import',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.import_batch',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// }
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
|
||||
).pipe(
|
||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
|
||||
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.menuService.addSection(this.menuID, {
|
||||
id: 'import_metadata',
|
||||
parentID: 'import',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '/admin/metadata-import'
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||
*/
|
||||
createSiteAdministratorMenuSections() {
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
||||
const menuList = [
|
||||
/* Admin Search */
|
||||
{
|
||||
id: 'admin_search',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.admin_search',
|
||||
link: '/admin/search'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'search',
|
||||
index: 5
|
||||
},
|
||||
/* Registries */
|
||||
{
|
||||
id: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.registries'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'list',
|
||||
index: 6
|
||||
},
|
||||
{
|
||||
id: 'registries_metadata',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_metadata',
|
||||
link: 'admin/registries/metadata'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'registries_format',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_format',
|
||||
link: 'admin/registries/bitstream-formats'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Curation tasks */
|
||||
{
|
||||
id: 'curation_tasks',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: 'admin/curation-tasks'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
},
|
||||
|
||||
/* Workflow */
|
||||
{
|
||||
id: 'workflow',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.workflow',
|
||||
link: '/admin/workflow'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
index: 11
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
||||
*/
|
||||
createAccessControlMenuSections() {
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
|
||||
).subscribe(([isSiteAdmin, canManageGroups]) => {
|
||||
const menuList = [
|
||||
/* Access Control */
|
||||
{
|
||||
id: 'access_control_people',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/access-control/epeople'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_groups',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: canManageGroups,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/access-control/groups'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'access_control_authorizations',
|
||||
// parentID: 'access_control',
|
||||
// active: false,
|
||||
// visible: authorized,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.access_control_authorizations',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
visible: canManageGroups || isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'key',
|
||||
index: 4
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('focusin')
|
||||
public handleFocusIn() {
|
||||
this.inFocus$.next(true);
|
||||
|
@@ -4,18 +4,18 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
|
||||
import { slide } from '../../../shared/animations/slide';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { bgColor } from '../../../shared/animations/bgColor';
|
||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Represents a expandable section in the sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* tslint:disable:component-selector */
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for AdminSidebarComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-admin-sidebar',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'AdminSidebarComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/admin/admin-sidebar/admin-sidebar.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./admin-sidebar.component');
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
|
||||
<div class="space-children-mr">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
|
||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
|
||||
</a>
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
|
||||
<i class="fa fa-hand-point-left"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -70,6 +70,12 @@ export function getWorkflowItemModuleRoute() {
|
||||
return `/${WORKFLOW_ITEM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export const WORKSPACE_ITEM_MODULE_PATH = 'workspaceitems';
|
||||
|
||||
export function getWorkspaceItemModuleRoute() {
|
||||
return `/${WORKSPACE_ITEM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export function getDSORoute(dso: DSpaceObject): string {
|
||||
if (hasValue(dso)) {
|
||||
switch ((dso as any).type) {
|
||||
@@ -101,6 +107,8 @@ export function getPageInternalServerErrorRoute() {
|
||||
return `/${INTERNAL_SERVER_ERROR}`;
|
||||
}
|
||||
|
||||
export const ERROR_PAGE = 'error';
|
||||
|
||||
export const INFO_MODULE_PATH = 'info';
|
||||
export function getInfoModulePath() {
|
||||
return `/${INFO_MODULE_PATH}`;
|
||||
@@ -116,3 +124,5 @@ export const REQUEST_COPY_MODULE_PATH = 'request-a-copy';
|
||||
export function getRequestCopyModulePath() {
|
||||
return `/${REQUEST_COPY_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export const HEALTH_PAGE_PATH = 'health';
|
||||
|
@@ -1,15 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RouterModule, NoPreloading } from '@angular/router';
|
||||
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
SiteAdministratorGuard
|
||||
} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
ACCESS_CONTROL_MODULE_PATH,
|
||||
ADMIN_MODULE_PATH,
|
||||
BITSTREAM_MODULE_PATH,
|
||||
ERROR_PAGE,
|
||||
FORBIDDEN_PATH,
|
||||
FORGOT_PASSWORD_PATH,
|
||||
HEALTH_PAGE_PATH,
|
||||
INFO_MODULE_PATH,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
LEGACY_BITSTREAM_MODULE_PATH,
|
||||
@@ -27,18 +31,26 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-
|
||||
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import {
|
||||
GroupAdministratorGuard
|
||||
} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||
import {
|
||||
ThemedPageInternalServerErrorComponent
|
||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
import { MenuResolver } from './menu.resolver';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||
{ path: ERROR_PAGE , component: ThemedPageErrorComponent },
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthBlockingGuard],
|
||||
canActivateChild: [ServerCheckGuard],
|
||||
resolve: [MenuResolver],
|
||||
children: [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
@@ -208,6 +220,11 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||
.then((m) => m.StatisticsPageRoutingModule)
|
||||
},
|
||||
{
|
||||
path: HEALTH_PAGE_PATH,
|
||||
loadChildren: () => import('./health-page/health-page.module')
|
||||
.then((m) => m.HealthPageModule)
|
||||
},
|
||||
{
|
||||
path: ACCESS_CONTROL_MODULE_PATH,
|
||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||
@@ -217,6 +234,12 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
]
|
||||
}
|
||||
], {
|
||||
// enableTracing: true,
|
||||
useHash: false,
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
initialNavigation: 'enabledBlocking',
|
||||
preloadingStrategy: NoPreloading,
|
||||
onSameUrlNavigation: 'reload',
|
||||
})
|
||||
],
|
||||
|
@@ -4,7 +4,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
||||
|
||||
// Load the implementations that should be tested
|
||||
import { AppComponent } from './app.component';
|
||||
@@ -32,7 +32,6 @@ 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';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
@@ -46,16 +45,16 @@ const initialState = {
|
||||
core: { auth: { loading: false } }
|
||||
};
|
||||
|
||||
export function getMockLocaleService(): LocaleService {
|
||||
return jasmine.createSpyObj('LocaleService', {
|
||||
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
||||
});
|
||||
}
|
||||
|
||||
describe('App component', () => {
|
||||
|
||||
let breadcrumbsServiceSpy;
|
||||
|
||||
function getMockLocaleService(): LocaleService {
|
||||
return jasmine.createSpyObj('LocaleService', {
|
||||
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
||||
});
|
||||
}
|
||||
|
||||
const getDefaultTestBedConf = () => {
|
||||
breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']);
|
||||
|
||||
@@ -130,66 +129,4 @@ describe('App component', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('the constructor', () => {
|
||||
it('should call breadcrumbsService.listenForRouteChanges', () => {
|
||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
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(getDefaultTestBedConf());
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when ThemeService returns a custom theme', () => {
|
||||
let document;
|
||||
let headSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
||||
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
||||
document = TestBed.inject(DOCUMENT);
|
||||
headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
|
||||
headSpy.getElementsByClassName.and.returnValue([]);
|
||||
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should append a link element with the correct attributes to the head element', () => {
|
||||
const link = document.createElement('link');
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('class', 'theme-css');
|
||||
link.setAttribute('href', '/custom-theme.css');
|
||||
|
||||
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { distinctUntilChanged, filter, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
@@ -7,47 +7,31 @@ import {
|
||||
HostListener,
|
||||
Inject,
|
||||
OnInit,
|
||||
Optional,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
NavigationStart, ResolveEnd,
|
||||
NavigationStart,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||
import { HostWindowState } from './shared/search/host-window.reducer';
|
||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { HostWindowService } from './shared/host-window.service';
|
||||
import { HeadTagConfig, ThemeConfig } from '../config/theme.model';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
import { environment } from '../environments/environment';
|
||||
import { models } from './core/core.module';
|
||||
import { LocaleService } from './core/locale/locale.service';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util';
|
||||
import { KlaroService } from './shared/cookies/klaro.service';
|
||||
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { getDefaultThemeConfig } from '../config/config.util';
|
||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { distinctNext } from './core/shared/distinct-next';
|
||||
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
@@ -56,11 +40,6 @@ import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
sidebarVisible: Observable<boolean>;
|
||||
slideSidebarOver: Observable<boolean>;
|
||||
collapsedSidebarWidth: Observable<string>;
|
||||
totalSidebarWidth: Observable<string>;
|
||||
theme: Observable<ThemeConfig> = of({} as any);
|
||||
notificationOptions;
|
||||
models;
|
||||
|
||||
@@ -72,14 +51,12 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
/**
|
||||
* Whether or not the app is in the process of rerouting
|
||||
*/
|
||||
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* Whether or not the theme is in the process of being swapped
|
||||
*/
|
||||
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
isThemeCSSLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
isThemeLoading$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Whether or not the idle modal is is currently open
|
||||
@@ -90,93 +67,45 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
@Inject(DOCUMENT) private document: any,
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
||||
private themeService: ThemeService,
|
||||
private translate: TranslateService,
|
||||
private store: Store<HostWindowState>,
|
||||
private metadata: MetadataService,
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private angulartics2DSpace: Angulartics2DSpace,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private cssService: CSSVariableService,
|
||||
private menuService: MenuService,
|
||||
private windowService: HostWindowService,
|
||||
private localeService: LocaleService,
|
||||
private breadcrumbsService: BreadcrumbsService,
|
||||
private modalService: NgbModal,
|
||||
@Optional() private cookiesService: KlaroService,
|
||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||
private modalConfig: NgbModalConfig,
|
||||
) {
|
||||
|
||||
if (!isEqual(environment, this.appConfig)) {
|
||||
throw new Error('environment does not match app config!');
|
||||
}
|
||||
|
||||
this.notificationOptions = environment.notifications;
|
||||
|
||||
/* Use models object so all decorators are actually called */
|
||||
this.models = models;
|
||||
|
||||
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
// the theme css will never download server side, so this should only happen on the browser
|
||||
this.isThemeCSSLoading$.next(true);
|
||||
}
|
||||
if (hasValue(themeName)) {
|
||||
this.loadGlobalThemeConfig(themeName);
|
||||
} else {
|
||||
const defaultThemeConfig = getDefaultThemeConfig();
|
||||
if (hasValue(defaultThemeConfig)) {
|
||||
this.loadGlobalThemeConfig(defaultThemeConfig.name);
|
||||
} else {
|
||||
this.loadGlobalThemeConfig(BASE_THEME_NAME);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.authService.trackTokenExpiration();
|
||||
this.trackIdleModal();
|
||||
}
|
||||
|
||||
// Load all the languages that are defined as active from the config file
|
||||
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||
this.isThemeLoading$ = this.themeService.isThemeLoading$;
|
||||
|
||||
// Load the default language from the config file
|
||||
// translate.setDefaultLang(environment.defaultLanguage);
|
||||
|
||||
// set the current language code
|
||||
this.localeService.setCurrentLanguageCode();
|
||||
|
||||
// analytics
|
||||
if (hasValue(googleAnalyticsService)) {
|
||||
googleAnalyticsService.addTrackingIdToPage();
|
||||
}
|
||||
angulartics2DSpace.startTracking();
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
breadcrumbsService.listenForRouteChanges();
|
||||
|
||||
if (environment.debug) {
|
||||
console.info(environment);
|
||||
}
|
||||
this.storeCSSVariables();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||
/** Implement behavior for interface {@link ModalBeforeDismiss} */
|
||||
this.modalConfig.beforeDismiss = async function () {
|
||||
if (typeof this?.componentInstance?.beforeDismiss === 'function') {
|
||||
return this.componentInstance.beforeDismiss();
|
||||
}
|
||||
|
||||
// fall back to default behavior
|
||||
return true;
|
||||
};
|
||||
|
||||
this.isAuthBlocking$ = this.store.pipe(
|
||||
select(isAuthenticationBlocking),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
this.isAuthBlocking$
|
||||
.pipe(
|
||||
filter((isBlocking: boolean) => isBlocking === false),
|
||||
take(1)
|
||||
).subscribe(() => this.initializeKlaro());
|
||||
|
||||
const env: string = environment.production ? 'Production' : 'Development';
|
||||
const color: string = environment.production ? 'red' : 'green';
|
||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||
}
|
||||
|
||||
@@ -196,34 +125,14 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
let resolveEndFound = false;
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
resolveEndFound = false;
|
||||
this.isRouteLoading$.next(true);
|
||||
this.isThemeLoading$.next(true);
|
||||
} else if (event instanceof ResolveEnd) {
|
||||
resolveEndFound = true;
|
||||
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;
|
||||
this.themeService.updateThemeOnRouteChange$(event.urlAfterRedirects, activatedRouteSnapShot).pipe(
|
||||
switchMap((changed) => {
|
||||
if (changed) {
|
||||
return this.isThemeCSSLoading$;
|
||||
} else {
|
||||
return [false];
|
||||
}
|
||||
})
|
||||
).subscribe((changed) => {
|
||||
this.isThemeLoading$.next(changed);
|
||||
});
|
||||
distinctNext(this.isRouteLoading$, true);
|
||||
} else if (
|
||||
event instanceof NavigationEnd ||
|
||||
event instanceof NavigationCancel
|
||||
) {
|
||||
if (!resolveEndFound) {
|
||||
this.isThemeLoading$.next(false);
|
||||
}
|
||||
this.isRouteLoading$.next(false);
|
||||
distinctNext(this.isRouteLoading$, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -239,125 +148,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
);
|
||||
}
|
||||
|
||||
private initializeKlaro() {
|
||||
if (hasValue(this.cookiesService)) {
|
||||
this.cookiesService.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private loadGlobalThemeConfig(themeName: string): void {
|
||||
this.setThemeCss(themeName);
|
||||
this.setHeadTags(themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the theme css file in <head>
|
||||
*
|
||||
* @param themeName The name of the new theme
|
||||
* @private
|
||||
*/
|
||||
private setThemeCss(themeName: string): void {
|
||||
const head = this.document.getElementsByTagName('head')[0];
|
||||
if (hasNoValue(head)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
|
||||
// automatically updated if we add nodes later
|
||||
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
|
||||
const link = this.document.createElement('link');
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('class', 'theme-css');
|
||||
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`);
|
||||
// wait for the new css to download before removing the old one to prevent a
|
||||
// flash of unstyled content
|
||||
link.onload = () => {
|
||||
if (isNotEmpty(currentThemeLinks)) {
|
||||
currentThemeLinks.forEach((currentThemeLink: any) => {
|
||||
if (hasValue(currentThemeLink)) {
|
||||
currentThemeLink.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
// the fact that this callback is used, proves we're on the browser.
|
||||
this.isThemeCSSLoading$.next(false);
|
||||
};
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
private setHeadTags(themeName: string): void {
|
||||
const head = this.document.getElementsByTagName('head')[0];
|
||||
if (hasNoValue(head)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// clear head tags
|
||||
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
|
||||
if (hasValue(currentHeadTags)) {
|
||||
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
|
||||
}
|
||||
|
||||
// create new head tags (not yet added to DOM)
|
||||
const headTagFragment = this.document.createDocumentFragment();
|
||||
this.createHeadTags(themeName)
|
||||
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
|
||||
|
||||
// add new head tags to DOM
|
||||
head.appendChild(headTagFragment);
|
||||
}
|
||||
|
||||
private createHeadTags(themeName: string): HTMLElement[] {
|
||||
const themeConfig = this.themeService.getThemeConfigFor(themeName);
|
||||
const headTagConfigs = themeConfig?.headTags;
|
||||
|
||||
if (hasNoValue(headTagConfigs)) {
|
||||
const parentThemeName = themeConfig?.extends;
|
||||
if (hasValue(parentThemeName)) {
|
||||
// inherit the head tags of the parent theme
|
||||
return this.createHeadTags(parentThemeName);
|
||||
}
|
||||
const defaultThemeConfig = getDefaultThemeConfig();
|
||||
const defaultThemeName = defaultThemeConfig.name;
|
||||
if (
|
||||
hasNoValue(defaultThemeName) ||
|
||||
themeName === defaultThemeName ||
|
||||
themeName === BASE_THEME_NAME
|
||||
) {
|
||||
// last resort, use fallback favicon.ico
|
||||
return [
|
||||
this.createHeadTag({
|
||||
'tagName': 'link',
|
||||
'attributes': {
|
||||
'rel': 'icon',
|
||||
'href': 'assets/images/favicon.ico',
|
||||
'sizes': 'any',
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
// inherit the head tags of the default theme
|
||||
return this.createHeadTags(defaultThemeConfig.name);
|
||||
}
|
||||
|
||||
return headTagConfigs.map(this.createHeadTag.bind(this));
|
||||
}
|
||||
|
||||
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
|
||||
const tag = this.document.createElement(headTagConfig.tagName);
|
||||
|
||||
if (hasValue(headTagConfig.attributes)) {
|
||||
Object.entries(headTagConfig.attributes)
|
||||
.forEach(([key, value]) => tag.setAttribute(key, value));
|
||||
}
|
||||
|
||||
// 'class' attribute should always be 'theme-head-tag' for removal
|
||||
tag.setAttribute('class', 'theme-head-tag');
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
private trackIdleModal() {
|
||||
const isIdle$ = this.authService.isUserIdle();
|
||||
const isAuthenticated$ = this.authService.isAuthenticated();
|
||||
@@ -376,4 +166,5 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
102
src/app/app.module.ts
Executable file → Normal file
102
src/app/app.module.ts
Executable file → Normal file
@@ -1,71 +1,46 @@
|
||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import {
|
||||
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
DYNAMIC_MATCHER_PROVIDERS,
|
||||
DynamicErrorMessagesMatcher
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
import { AdminSidebarSectionComponent } from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
|
||||
import { ExpandableAdminSidebarSectionComponent } from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { appEffects } from './app.effects';
|
||||
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
||||
import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
||||
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { NavbarModule } from './navbar/navbar.module';
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||
import { environment } from '../environments/environment';
|
||||
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
||||
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
||||
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||
import { LogInterceptor } from './core/log/log.interceptor';
|
||||
import { RootComponent } from './root/root.component';
|
||||
import { ThemedRootComponent } from './root/themed-root.component';
|
||||
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
|
||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { ThemedHeaderComponent } from './header/themed-header.component';
|
||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
|
||||
|
||||
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||
import { NgxMaskModule } from 'ngx-mask';
|
||||
import { StoreDevModules } from '../config/store/devtools';
|
||||
import { RootModule } from './root.module';
|
||||
|
||||
export function getConfig() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
export function getBase(appConfig: AppConfig) {
|
||||
return appConfig.ui.nameSpace;
|
||||
}
|
||||
const getBaseHref = (document: Document, appConfig: AppConfig): string => {
|
||||
const baseTag = document.querySelector('head > base');
|
||||
baseTag.setAttribute('href', `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`);
|
||||
return baseTag.getAttribute('href');
|
||||
};
|
||||
|
||||
export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
|
||||
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||
@@ -89,28 +64,20 @@ const IMPORTS = [
|
||||
ScrollToModule.forRoot(),
|
||||
NgbModule,
|
||||
TranslateModule.forRoot(),
|
||||
NgxMaskModule.forRoot(),
|
||||
EffectsModule.forRoot(appEffects),
|
||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||
StoreRouterConnectingModule.forRoot(),
|
||||
ThemedEntryComponentModule.withEntryComponents(),
|
||||
StoreDevModules,
|
||||
EagerThemesModule,
|
||||
RootModule,
|
||||
];
|
||||
|
||||
IMPORTS.push(
|
||||
StoreDevtoolsModule.instrument({
|
||||
maxAge: 1000,
|
||||
logOnly: environment.production,
|
||||
})
|
||||
);
|
||||
|
||||
const PROVIDERS = [
|
||||
{
|
||||
provide: APP_CONFIG,
|
||||
useFactory: getConfig
|
||||
},
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useFactory: getBase,
|
||||
deps: [APP_CONFIG]
|
||||
useFactory: getBaseHref,
|
||||
deps: [DOCUMENT, APP_CONFIG]
|
||||
},
|
||||
{
|
||||
provide: USER_PROVIDED_META_REDUCERS,
|
||||
@@ -122,15 +89,6 @@ const PROVIDERS = [
|
||||
useClass: DSpaceRouterStateSerializer
|
||||
},
|
||||
ClientCookieService,
|
||||
// Check the authentication token when the app initializes
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: (store: Store<AppState>,) => {
|
||||
return () => store.dispatch(new CheckAuthenticationTokenAction());
|
||||
},
|
||||
deps: [Store],
|
||||
multi: true
|
||||
},
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
@@ -164,28 +122,6 @@ const PROVIDERS = [
|
||||
|
||||
const DECLARATIONS = [
|
||||
AppComponent,
|
||||
RootComponent,
|
||||
ThemedRootComponent,
|
||||
HeaderComponent,
|
||||
ThemedHeaderComponent,
|
||||
HeaderNavbarWrapperComponent,
|
||||
ThemedHeaderNavbarWrapperComponent,
|
||||
AdminSidebarComponent,
|
||||
AdminSidebarSectionComponent,
|
||||
ExpandableAdminSidebarSectionComponent,
|
||||
FooterComponent,
|
||||
ThemedFooterComponent,
|
||||
PageNotFoundComponent,
|
||||
ThemedPageNotFoundComponent,
|
||||
NotificationComponent,
|
||||
NotificationsBoardComponent,
|
||||
BreadcrumbsComponent,
|
||||
ThemedBreadcrumbsComponent,
|
||||
ForbiddenComponent,
|
||||
ThemedForbiddenComponent,
|
||||
IdleModalComponent,
|
||||
ThemedPageInternalServerErrorComponent,
|
||||
PageInternalServerErrorComponent
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
nameVariantReducer
|
||||
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||
import { menusReducer} from './shared/menu/menu.reducer';
|
||||
import {
|
||||
notificationsReducer,
|
||||
NotificationsState
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||
import { MenusState } from './shared/menu/menus-state.model';
|
||||
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
||||
|
||||
export interface AppState {
|
||||
|
@@ -10,6 +10,9 @@ import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/re
|
||||
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||
import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver';
|
||||
import { BitstreamBreadcrumbsService } from '../core/breadcrumbs/bitstream-breadcrumbs.service';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||
@@ -48,7 +51,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||
path: EDIT_BITSTREAM_PATH,
|
||||
component: EditBitstreamPageComponent,
|
||||
resolve: {
|
||||
bitstream: BitstreamPageResolver
|
||||
bitstream: BitstreamPageResolver,
|
||||
breadcrumb: BitstreamBreadcrumbResolver,
|
||||
},
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
@@ -67,15 +71,17 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||
{
|
||||
path: 'edit',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
resourcePolicy: ResourcePolicyResolver
|
||||
},
|
||||
component: ResourcePolicyEditComponent,
|
||||
data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true }
|
||||
data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true }
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
resolve: {
|
||||
bitstream: BitstreamPageResolver
|
||||
bitstream: BitstreamPageResolver,
|
||||
breadcrumb: BitstreamBreadcrumbResolver,
|
||||
},
|
||||
component: BitstreamAuthorizationsComponent,
|
||||
data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }
|
||||
@@ -86,6 +92,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||
],
|
||||
providers: [
|
||||
BitstreamPageResolver,
|
||||
BitstreamBreadcrumbResolver,
|
||||
BitstreamBreadcrumbsService
|
||||
]
|
||||
})
|
||||
export class BitstreamPageRoutingModule {
|
||||
|
@@ -7,6 +7,15 @@ import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
|
||||
/**
|
||||
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||
* Requesting them as embeds will limit the number of requests
|
||||
*/
|
||||
export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
|
||||
followLink('bundle', {}, followLink('item')),
|
||||
followLink('format')
|
||||
];
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific bitstream before the route is activated
|
||||
*/
|
||||
@@ -34,9 +43,6 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
|
||||
* Requesting them as embeds will limit the number of requests
|
||||
*/
|
||||
get followLinks(): FollowLinkConfig<Bitstream>[] {
|
||||
return [
|
||||
followLink('bundle', {}, followLink('item')),
|
||||
followLink('format')
|
||||
];
|
||||
return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
|
||||
}
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -2,8 +2,8 @@ import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||
import { of as observableOf, EMPTY } from 'rxjs';
|
||||
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { RequestEntryState } from '../core/data/request.reducer';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RequestEntryState } from '../core/data/request-entry-state.model';
|
||||
|
||||
describe(`LegacyBitstreamUrlResolver`, () => {
|
||||
let resolver: LegacyBitstreamUrlResolver;
|
||||
|
@@ -20,9 +20,9 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
|
||||
describe('BrowseByDatePageComponent', () => {
|
||||
let comp: BrowseByDatePageComponent;
|
||||
|
@@ -18,6 +18,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { isValidDate } from '../../shared/date.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -85,10 +86,10 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
let lowerLimit = environment.browseBy.defaultLowerLimit;
|
||||
if (hasValue(firstItemRD.payload)) {
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
|
||||
if (hasValue(date)) {
|
||||
if (isNotEmpty(date) && isValidDate(date)) {
|
||||
const dateObj = new Date(date);
|
||||
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
|
||||
lowerLimit = dateObj.getUTCFullYear();
|
||||
lowerLimit = isNaN(dateObj.getUTCFullYear()) ? lowerLimit : dateObj.getUTCFullYear();
|
||||
}
|
||||
}
|
||||
const options = [];
|
||||
|
@@ -18,24 +18,29 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-comcol-page-browse-by>
|
||||
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
|
||||
</ng-container></ng-container>
|
||||
|
||||
<section class="comcol-page-browse-section">
|
||||
<div class="browse-by-metadata w-100">
|
||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||
title="{{'browse.title' | translate:
|
||||
{
|
||||
collection: (parent$ | async)?.payload?.name || '',
|
||||
field: 'browse.metadata.' + browseId | translate,
|
||||
startsWith: (startsWith)? ('browse.startsWith' | translate: { startsWith: '"' + startsWith + '"' }) : '',
|
||||
value: (value)? '"' + value + '"': ''
|
||||
} }}"
|
||||
parentname="{{(parent$ | async)?.payload?.name || ''}}"
|
||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||
[paginationConfig]="(currentPagination$ |async)"
|
||||
[sortConfig]="(currentSort$ |async)"
|
||||
[type]="startsWithType"
|
||||
[startsWithOptions]="startsWithOptions"
|
||||
[enableArrows]="true"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()">
|
||||
</ds-browse-by>
|
||||
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</section>
|
||||
<ng-container *ngVar="(parent$ | async) as parent">
|
||||
|
@@ -18,6 +18,8 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||
@@ -50,7 +52,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'bbm',
|
||||
id: BBM_PAGINATION_ID,
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
@@ -127,10 +129,10 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
||||
})
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.authority = params.authority;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(searchOptions, this.value, this.authority);
|
||||
|
@@ -17,7 +17,7 @@ import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-bro
|
||||
component: ThemedBrowseBySwitcherComponent,
|
||||
canActivate: [BrowseByGuard],
|
||||
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
|
||||
data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' }
|
||||
}
|
||||
]
|
||||
}])
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
resolveTheme
|
||||
} from '../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
|
||||
export enum BrowseByDataType {
|
||||
Title = 'title',
|
||||
@@ -10,7 +14,7 @@ export enum BrowseByDataType {
|
||||
|
||||
export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata;
|
||||
|
||||
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType, theme) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||
providedIn: 'root',
|
||||
factory: () => getComponentByBrowseByType
|
||||
});
|
||||
@@ -20,13 +24,17 @@ const map = new Map();
|
||||
/**
|
||||
* Decorator used for rendering Browse-By pages by type
|
||||
* @param browseByType The type of page
|
||||
* @param theme The optional theme for the component
|
||||
*/
|
||||
export function rendersBrowseBy(browseByType: BrowseByDataType) {
|
||||
export function rendersBrowseBy(browseByType: BrowseByDataType, theme = DEFAULT_THEME) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(map.get(browseByType))) {
|
||||
map.set(browseByType, component);
|
||||
map.set(browseByType, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(browseByType).get(theme))) {
|
||||
map.get(browseByType).set(theme, component);
|
||||
} else {
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}"`);
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}" and theme "${theme}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -34,11 +42,16 @@ export function rendersBrowseBy(browseByType: BrowseByDataType) {
|
||||
/**
|
||||
* Get the component used for rendering a Browse-By page by type
|
||||
* @param browseByType The type of page
|
||||
* @param theme the theme to match
|
||||
*/
|
||||
export function getComponentByBrowseByType(browseByType) {
|
||||
const comp = map.get(browseByType);
|
||||
export function getComponentByBrowseByType(browseByType, theme) {
|
||||
let themeMap = map.get(browseByType);
|
||||
if (hasNoValue(themeMap)) {
|
||||
themeMap = map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
}
|
||||
const comp = resolveTheme(themeMap, theme);
|
||||
if (hasNoValue(comp)) {
|
||||
map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
return themeMap.get(DEFAULT_THEME);
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { BehaviorSubject, of as observableOf } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
describe('BrowseBySwitcherComponent', () => {
|
||||
let comp: BrowseBySwitcherComponent;
|
||||
@@ -44,11 +45,20 @@ describe('BrowseBySwitcherComponent', () => {
|
||||
data
|
||||
};
|
||||
|
||||
let themeService: ThemeService;
|
||||
let themeName: string;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeName = 'dspace';
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: themeName,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseBySwitcherComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -68,7 +78,7 @@ describe('BrowseBySwitcherComponent', () => {
|
||||
});
|
||||
|
||||
it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => {
|
||||
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType);
|
||||
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType, themeName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,7 @@ import { map } from 'rxjs/operators';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
@@ -21,7 +22,8 @@ export class BrowseBySwitcherComponent implements OnInit {
|
||||
browseByComponent: Observable<any>;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor<any>) {
|
||||
protected themeService: ThemeService,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseByComponent = this.route.data.pipe(
|
||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType))
|
||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType, this.themeService.getThemeName()))
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -20,9 +20,9 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
|
||||
describe('BrowseByTitlePageComponent', () => {
|
||||
let comp: BrowseByTitlePageComponent;
|
||||
|
@@ -45,7 +45,8 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
||||
})
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined);
|
||||
this.updateParent(params.scope);
|
||||
}));
|
||||
|
@@ -16,7 +16,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
import { EntityTypeService } from '../../core/data/entity-type.service';
|
||||
import { EntityTypeDataService } from '../../core/data/entity-type-data.service';
|
||||
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
|
||||
@@ -61,7 +61,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected entityTypeService: EntityTypeService) {
|
||||
protected entityTypeService: EntityTypeDataService) {
|
||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ import { Collection } from '../../core/shared/collection.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, EventEmitter } from '@angular/core';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -41,6 +41,12 @@ import {
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
||||
import { GroupDataService } from '../../core/eperson/group-data.service';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
|
||||
|
||||
describe('CollectionItemMapperComponent', () => {
|
||||
let comp: CollectionItemMapperComponent;
|
||||
@@ -88,7 +94,7 @@ describe('CollectionItemMapperComponent', () => {
|
||||
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||
const itemDataServiceStub = {
|
||||
mapToCollection: () => createSuccessfulRemoteDataObject$({}),
|
||||
findAllByHref: () => observableOf(emptyList)
|
||||
findListByHref: () => observableOf(emptyList),
|
||||
};
|
||||
const activatedRouteStub = {
|
||||
parent: {
|
||||
@@ -110,15 +116,15 @@ describe('CollectionItemMapperComponent', () => {
|
||||
};
|
||||
const searchServiceStub = Object.assign(new SearchServiceStub(), {
|
||||
search: () => observableOf(emptyList),
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
clearDiscoveryRequests: () => {}
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty,@typescript-eslint/no-empty-function */
|
||||
});
|
||||
const collectionDataServiceStub = {
|
||||
getMappedItems: () => observableOf(emptyList),
|
||||
/* tslint:disable:no-empty */
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
clearMappedItemsRequests: () => {}
|
||||
/* tslint:enable:no-empty */
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
};
|
||||
const routeServiceStub = {
|
||||
getRouteParameterValue: () => {
|
||||
@@ -141,6 +147,25 @@ describe('CollectionItemMapperComponent', () => {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
|
||||
addTag: ''
|
||||
});
|
||||
|
||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: '',
|
||||
getUUIDFromString: '',
|
||||
});
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'test',
|
||||
values: [
|
||||
'org.dspace.ctask.general.ProfileFormats = test'
|
||||
]
|
||||
}))
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -157,8 +182,19 @@ describe('CollectionItemMapperComponent', () => {
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService }
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{ provide: GroupDataService, useValue: groupDataService },
|
||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
]
|
||||
}).overrideComponent(CollectionItemMapperComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationServiceStub
|
||||
}
|
||||
] }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@@ -143,7 +143,7 @@ export class CollectionItemMapperComponent implements OnInit {
|
||||
if (shouldUpdate === true) {
|
||||
this.shouldUpdate$.next(false);
|
||||
}
|
||||
return this.itemDataService.findAllByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
||||
return this.itemDataService.findListByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
||||
sort: this.defaultSortOptions
|
||||
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
|
||||
getAllSucceededRemoteData()
|
||||
|
@@ -6,7 +6,7 @@ import { CreateCollectionPageComponent } from './create-collection-page/create-c
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
||||
import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver';
|
||||
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
COLLECTION_CREATE_PATH
|
||||
} from './collection-page-routing-paths';
|
||||
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -52,7 +52,7 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
|
||||
},
|
||||
{
|
||||
path: ITEMTEMPLATE_PATH,
|
||||
component: EditItemTemplatePageComponent,
|
||||
component: ThemedEditItemTemplatePageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
item: ItemTemplatePageResolver,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<div class="container">
|
||||
<div class="collection-page"
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
||||
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
||||
<header class="comcol-header mr-auto">
|
||||
@@ -13,8 +13,7 @@
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
[logo]="(logoRD$ | async)?.payload"
|
||||
[alternateText]="'Collection Logo'"
|
||||
[alternateText]="'Collection Logo'">
|
||||
[alternateText]="'Collection Logo'">
|
||||
</ds-comcol-page-logo>
|
||||
|
||||
<!-- Handle -->
|
||||
@@ -34,16 +33,16 @@
|
||||
[title]="'collection.page.news'">
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by
|
||||
<ds-themed-comcol-page-browse-by
|
||||
[id]="collection.id"
|
||||
[contentType]="collection.type">
|
||||
</ds-comcol-page-browse-by>
|
||||
</ds-themed-comcol-page-browse-by>
|
||||
|
||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||
@@ -57,8 +56,8 @@
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed"
|
||||
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
|
||||
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
|
||||
{{'collection.page.browse.recent.empty' | translate}}
|
||||
</div>
|
||||
@@ -75,7 +74,7 @@
|
||||
</div>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||
message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="collectionRD?.isLoading"
|
||||
message="{{'loading.collection' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="collectionRD?.isLoading"
|
||||
message="{{'loading.collection' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -16,7 +16,6 @@ import { Item } from '../core/shared/item.model';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
redirectOn4xx,
|
||||
toDSpaceObjectListRD
|
||||
} from '../core/shared/operators';
|
||||
|
||||
@@ -28,6 +27,7 @@ import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-page',
|
||||
|
@@ -8,6 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
||||
import { EditItemPageModule } from '../item-page/edit-item-page/edit-item-page.module';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
@@ -32,6 +33,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
CreateCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
EditItemTemplatePageComponent,
|
||||
ThemedEditItemTemplatePageComponent,
|
||||
CollectionItemMapperComponent
|
||||
],
|
||||
providers: [
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<div class="col text-right space-children-mr">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
|
||||
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -5,7 +5,6 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Collection
|
||||
@@ -24,8 +23,7 @@ export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Col
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected requestService: RequestService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate, requestService);
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="container-fluid mb-2" *ngVar="(itemTemplateRD$ | async) as itemTemplateRD">
|
||||
<label>{{ 'collection.edit.template.label' | translate}}</label>
|
||||
<div class="button-row">
|
||||
<div class="button-row space-children-mr">
|
||||
<button *ngIf="!itemTemplateRD?.payload" class="btn btn-success" (click)="addItemTemplate()">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"collection.edit.template.add-button" | translate}}</span>
|
||||
|
@@ -12,9 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
describe('CollectionMetadataComponent', () => {
|
||||
@@ -40,8 +39,8 @@ describe('CollectionMetadataComponent', () => {
|
||||
|
||||
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||
create: createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: observableOf(true),
|
||||
createByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||
delete: observableOf(true),
|
||||
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
||||
});
|
||||
|
||||
@@ -49,9 +48,6 @@ describe('CollectionMetadataComponent', () => {
|
||||
success: {},
|
||||
error: {}
|
||||
});
|
||||
const objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: {}
|
||||
});
|
||||
const requestService = jasmine.createSpyObj('requestService', {
|
||||
setStaleByHrefSubstring: {}
|
||||
});
|
||||
@@ -65,8 +61,7 @@ describe('CollectionMetadataComponent', () => {
|
||||
{ provide: ItemTemplateDataService, useValue: itemTemplateServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService }
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -95,26 +90,24 @@ describe('CollectionMetadataComponent', () => {
|
||||
});
|
||||
|
||||
describe('deleteItemTemplate', () => {
|
||||
describe('when delete returns a success', () => {
|
||||
beforeEach(() => {
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
beforeEach(() => {
|
||||
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
it('should call ItemTemplateService.delete', () => {
|
||||
expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid);
|
||||
});
|
||||
|
||||
describe('when delete returns a success', () => {
|
||||
it('should display a success notification', () => {
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset related object and request cache', () => {
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when delete returns a failure', () => {
|
||||
beforeEach(() => {
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
|
@@ -7,13 +7,14 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Component for editing a collection's metadata
|
||||
@@ -38,8 +39,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
protected route: ActivatedRoute,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected requestService: RequestService
|
||||
protected requestService: RequestService,
|
||||
) {
|
||||
super(collectionDataService, router, route, notificationsService, translate);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid).pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.createByCollectionID(new Item(), collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
@@ -85,32 +85,15 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
* Delete the item template from the collection
|
||||
*/
|
||||
deleteItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$, templateHref$).pipe(
|
||||
switchMap(([collection, template, templateHref]) => {
|
||||
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe(
|
||||
tap((success: boolean) => {
|
||||
if (success) {
|
||||
this.objectCache.remove(templateHref);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.requestService.setStaleByHrefSubstring(collection.self);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((template) => {
|
||||
return this.itemTemplateService.delete(template.uuid);
|
||||
}),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded),
|
||||
).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success'));
|
||||
|
@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ComcolModule } from '../../../shared/comcol/comcol.module';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
|
||||
describe('CollectionRolesComponent', () => {
|
||||
|
||||
@@ -79,6 +81,7 @@ describe('CollectionRolesComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: GroupDataService, useValue: groupDataService },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div *ngVar="(contentSource$ |async) as contentSource">
|
||||
<div class="container-fluid" *ngIf="shouldShow">
|
||||
<div class="container-fluid space-children-mr" *ngIf="shouldShow">
|
||||
<h4>{{ 'collection.source.controls.head' | translate }}</h4>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.status' | translate}}</span>
|
||||
@@ -51,4 +51,4 @@
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container-fluid">
|
||||
<div class="d-inline-block float-right">
|
||||
<div class="d-inline-block float-right space-children-mr">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
@@ -25,7 +25,7 @@
|
||||
<label class="form-check-label"
|
||||
for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||
</div>
|
||||
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-themed-loading>
|
||||
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="container mt-2" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-inline-block float-right ml-1">
|
||||
<div class="d-inline-block float-right ml-1 space-children-mr">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
|
@@ -9,7 +9,6 @@ import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/co
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
|
||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||
import { FieldUpdate } from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
@@ -20,6 +19,7 @@ import { Collection } from '../../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||
|
||||
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
||||
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||
|
@@ -23,7 +23,6 @@ import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { first, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
@@ -31,6 +30,8 @@ import { MetadataConfig } from '../../../core/shared/metadata-config.model';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||
|
||||
/**
|
||||
* Component for managing the content source of the collection
|
||||
|
@@ -3,10 +3,10 @@
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
|
||||
<ds-themed-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-themed-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>
|
||||
<ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-item-template-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for editing the item template of a collection
|
||||
*/
|
||||
export class ThemedEditItemTemplatePageComponent extends ThemedComponent<EditItemTemplatePageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditItemTemplatePageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/collection-page/edit-item-template-page/edit-item-template-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-item-template-page.component');
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ import { CollectionPageComponent } from './collection-page.component';
|
||||
* Themed wrapper for CollectionPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-page',
|
||||
selector: 'ds-themed-collection-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { CommunityListService, FlatNode } from './community-list-service';
|
||||
import { CommunityListService} from './community-list-service';
|
||||
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { FlatNode } from './flat-node.model';
|
||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||
|
||||
/**
|
||||
* DataSource object needed by a CDK Tree to render its nodes.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
||||
<ds-community-list></ds-community-list>
|
||||
<ds-themed-community-list></ds-themed-community-list>
|
||||
</div>
|
||||
|
@@ -5,12 +5,14 @@ import { CommunityListPageComponent } from './community-list-page.component';
|
||||
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
||||
import { CommunityListComponent } from './community-list/community-list.component';
|
||||
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
|
||||
import { ThemedCommunityListComponent } from './community-list/themed-community-list.component';
|
||||
|
||||
|
||||
const DECLARATIONS = [
|
||||
CommunityListPageComponent,
|
||||
CommunityListComponent,
|
||||
ThemedCommunityListPageComponent
|
||||
ThemedCommunityListPageComponent,
|
||||
ThemedCommunityListComponent
|
||||
];
|
||||
/**
|
||||
* The page which houses a title and the community list, as described in community-list.component
|
||||
|
@@ -7,13 +7,14 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { StoreMock } from '../shared/testing/store.mock';
|
||||
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
||||
import { CommunityListService, toFlatNode } from './community-list-service';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
import { FlatNode } from './flat-node.model';
|
||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||
|
||||
describe('CommunityListService', () => {
|
||||
let store: StoreMock<AppState>;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { createSelector, Store } from '@ngrx/store';
|
||||
|
||||
@@ -6,45 +7,22 @@ import { filter, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AppState } from '../app.reducer';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { PaginatedList, buildPaginatedList } from '../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { CommunityListSaveAction } from './community-list.actions';
|
||||
import { CommunityListState } from './community-list.reducer';
|
||||
import { getCommunityPageRoute } from '../community-page/community-page-routing-paths';
|
||||
import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths';
|
||||
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../core/shared/operators';
|
||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and
|
||||
* state in the tree. There are nodes representing communities, collections and show more links.
|
||||
*/
|
||||
export interface FlatNode {
|
||||
isExpandable$: Observable<boolean>;
|
||||
name: string;
|
||||
id: string;
|
||||
level: number;
|
||||
isExpanded?: boolean;
|
||||
parent?: FlatNode;
|
||||
payload: Community | Collection | ShowMoreFlatNode;
|
||||
isShowMoreNode: boolean;
|
||||
route?: string;
|
||||
currentCommunityPage?: number;
|
||||
currentCollectionPage?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The show more links in the community tree are also represented by a flatNode so we know where in
|
||||
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
|
||||
*/
|
||||
export class ShowMoreFlatNode {
|
||||
}
|
||||
import { FlatNode } from './flat-node.model';
|
||||
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||
|
||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||
@@ -108,7 +86,6 @@ export const MAX_COMCOLS_PER_PAGE = 20;
|
||||
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
||||
* and connection to the store to retrieve and save the state of the community list
|
||||
*/
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
@Injectable()
|
||||
export class CommunityListService {
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../shared/ngrx/type';
|
||||
import { FlatNode } from './community-list-service';
|
||||
import { FlatNode } from './flat-node.model';
|
||||
|
||||
/**
|
||||
* All the action types of the community-list
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FlatNode } from './community-list-service';
|
||||
import { CommunityListActions, CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions';
|
||||
import { FlatNode } from './flat-node.model';
|
||||
|
||||
/**
|
||||
* States we wish to put in store concerning the community list
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<!-- This is the tree node template for show more node -->
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
||||
@@ -8,11 +8,11 @@
|
||||
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="align-middle pt-2">
|
||||
<a *ngIf="node!==loadingNode" [routerLink]="" (click)="getNextPage(node)"
|
||||
<a *ngIf="node!==loadingNode" [routerLink]="[]" (click)="getNextPage(node)"
|
||||
class="btn btn-outline-primary btn-sm" role="button">
|
||||
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
||||
</a>
|
||||
<ds-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-themed-loading"></ds-themed-loading>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted" cdkTreeNodePadding>
|
||||
@@ -57,7 +57,7 @@
|
||||
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<ds-loading class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading class="ds-themed-loading"></ds-themed-loading>
|
||||
</div>
|
||||
</cdk-tree-node>
|
||||
<!-- This is the tree node template for leaf nodes (collections and (sub)coms without children) -->
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { CommunityListComponent } from './community-list.component';
|
||||
import { CommunityListService, FlatNode, showMoreFlatNode, toFlatNode } from '../community-list-service';
|
||||
import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community-list-service';
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
@@ -15,6 +15,7 @@ import { Collection } from '../../core/shared/collection.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { FlatNode } from '../flat-node.model';
|
||||
|
||||
describe('CommunityListComponent', () => {
|
||||
let component: CommunityListComponent;
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { CommunityListService, FlatNode } from '../community-list-service';
|
||||
import { CommunityListService} from '../community-list-service';
|
||||
import { CommunityListDatasource } from '../community-list-datasource';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
import { FlatNode } from '../flat-node.model';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
|
||||
/**
|
||||
* A tree-structured list of nodes representing the communities, their subCommunities and collections.
|
||||
|
@@ -0,0 +1,24 @@
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { CommunityListComponent } from './community-list.component';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-community-list',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CommunityListComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/community-list-page/community-list/community-list.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./community-list.component`);
|
||||
}
|
||||
|
||||
}
|
22
src/app/community-list-page/flat-node.model.ts
Normal file
22
src/app/community-list-page/flat-node.model.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||
|
||||
/**
|
||||
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and
|
||||
* state in the tree. There are nodes representing communities, collections and show more links.
|
||||
*/
|
||||
export interface FlatNode {
|
||||
isExpandable$: Observable<boolean>;
|
||||
name: string;
|
||||
id: string;
|
||||
level: number;
|
||||
isExpanded?: boolean;
|
||||
parent?: FlatNode;
|
||||
payload: Community | Collection | ShowMoreFlatNode;
|
||||
isShowMoreNode: boolean;
|
||||
route?: string;
|
||||
currentCommunityPage?: number;
|
||||
currentCollectionPage?: number;
|
||||
}
|
6
src/app/community-list-page/show-more-flat-node.model.ts
Normal file
6
src/app/community-list-page/show-more-flat-node.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* The show more links in the community tree are also represented by a flatNode so we know where in
|
||||
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
|
||||
*/
|
||||
export class ShowMoreFlatNode {
|
||||
}
|
@@ -11,9 +11,9 @@ import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.servi
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
||||
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@@ -20,14 +20,14 @@
|
||||
[title]="'community.page.news'">
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||
</ds-comcol-page-browse-by>
|
||||
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||
</ds-themed-comcol-page-browse-by>
|
||||
|
||||
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||
@@ -41,5 +41,5 @@
|
||||
</div>
|
||||
|
||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
|
@@ -13,11 +13,12 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators';
|
||||
import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
|
||||
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 { getCommunityPageRoute } from './community-page-routing-paths';
|
||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page',
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<div class="col text-right space-children-mr">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span>
|
||||
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -5,7 +5,6 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Community
|
||||
@@ -24,9 +23,8 @@ export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Comm
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected requestService: RequestService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate, requestService);
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ComcolModule } from '../../../shared/comcol/comcol.module';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
|
||||
describe('CommunityRolesComponent', () => {
|
||||
|
||||
@@ -64,6 +66,7 @@ describe('CommunityRolesComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: GroupDataService, useValue: groupDataService },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -9,5 +9,5 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-themed-loading>
|
||||
</ng-container>
|
||||
|
@@ -11,7 +11,6 @@ import { CommunityPageSubCollectionListComponent } from './community-page-sub-co
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
@@ -25,6 +24,15 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
import { GroupDataService } from '../../core/eperson/group-data.service';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
|
||||
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
||||
|
||||
describe('CommunityPageSubCollectionList Component', () => {
|
||||
let comp: CommunityPageSubCollectionListComponent;
|
||||
@@ -122,6 +130,25 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
|
||||
themeService = getMockThemeService();
|
||||
|
||||
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
|
||||
addTag: ''
|
||||
});
|
||||
|
||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: '',
|
||||
getUUIDFromString: '',
|
||||
});
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'test',
|
||||
values: [
|
||||
'org.dspace.ctask.general.ProfileFormats = test'
|
||||
]
|
||||
}))
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -138,6 +165,10 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
{ provide: GroupDataService, useValue: groupDataService },
|
||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -9,5 +9,5 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-themed-loading>
|
||||
</ng-container>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user