Merge remote-tracking branch 'upstream/main' into retrieve-name-with-dsonameservice-7.4

# Conflicts:
#	src/app/access-control/epeople-registry/epeople-registry.component.html
#	src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts
#	src/app/access-control/group-registry/group-form/group-form.component.spec.ts
#	src/app/access-control/group-registry/group-form/group-form.component.ts
#	src/app/access-control/group-registry/group-form/members-list/members-list.component.html
#	src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
#	src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html
#	src/app/item-page/full/field-components/file-section/full-file-section.component.ts
#	src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts
#	src/app/item-page/simple/field-components/file-section/file-section.component.html
#	src/app/item-page/simple/field-components/file-section/file-section.component.ts
#	src/app/item-page/versions/item-versions.component.ts
#	src/app/shared/auth-nav-menu/user-menu/user-menu.component.html
#	src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts
#	src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html
This commit is contained in:
Alexandre Vryghem
2023-03-02 21:15:00 +01:00
960 changed files with 41545 additions and 19578 deletions

View File

@@ -10,6 +10,16 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { FormModule } from '../shared/form/form.module';
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
import { AbstractControl } from '@angular/forms';
/**
* Condition for displaying error messages on email form field
*/
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
};
@NgModule({
imports: [
@@ -17,7 +27,10 @@ import { FormModule } from '../shared/form/form.module';
SharedModule,
RouterModule,
AccessControlRoutingModule,
FormModule
FormModule,
],
exports: [
MembersListComponent,
],
declarations: [
EPeopleRegistryComponent,
@@ -25,7 +38,13 @@ import { FormModule } from '../shared/form/form.module';
GroupsRegistryComponent,
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent
MembersListComponent,
],
providers: [
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher
},
]
})
/**

View File

@@ -8,7 +8,7 @@
<button class="mr-auto btn btn-success addEPerson-button"
(click)="isEPersonFormShown = true">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
</button>
</div>
</div>
@@ -30,7 +30,7 @@
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" attr.aria-label="{{labelPrefix + 'search.placeholder' | translate}}"
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-primary">
@@ -72,13 +72,13 @@
<td>{{epersonDto.eperson.email}}</td>
<td>
<div class="btn-group edit-field">
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
<button (click)="toggleEditEPerson(epersonDto.eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>

View File

@@ -1,7 +1,7 @@
import { Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
@@ -42,6 +42,7 @@ describe('EPeopleRegistryComponent', () => {
let paginationService;
beforeEach(waitForAsync(() => {
jasmine.getEnv().allowRespy(true);
mockEPeople = [EPersonMock, EPersonMock2];
ePersonDataServiceStub = {
activeEPerson: null,
@@ -98,7 +99,7 @@ describe('EPeopleRegistryComponent', () => {
deleteEPerson(ePerson: EPerson): Observable<boolean> {
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
return (ePerson2.uuid !== ePerson.uuid);
});
});
return observableOf(true);
},
editEPerson(ePerson: EPerson) {
@@ -260,17 +261,16 @@ describe('EPeopleRegistryComponent', () => {
describe('delete EPerson button when the isAuthorized returns false', () => {
let ePeopleDeleteButton;
beforeEach(() => {
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(false)
});
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
component.initialisePage();
fixture.detectChanges();
});
it('should be disabled', () => {
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
ePeopleDeleteButton.forEach((deleteButton) => {
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
expect(deleteButton.nativeElement.disabled).toBe(true);
});
});
});
});

View File

@@ -537,7 +537,7 @@ describe('EPersonFormComponent', () => {
});
it('should call epersonRegistrationService.registerEmail', () => {
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail, null, 'forgot');
});
});
});

View File

@@ -36,6 +36,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { Registration } from '../../../core/shared/registration.model';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
@Component({
@@ -493,7 +494,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/
resetPassword() {
if (hasValue(this.epersonInitial.email)) {
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null, TYPE_REQUEST_FORGOT).pipe(getFirstCompletedRemoteData())
.subscribe((response: RemoteData<Registration>) => {
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),

View File

@@ -9,7 +9,18 @@
</ng-template>
<ng-template #editheader>
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.edit' | translate}}</h2>
<h2 class="border-bottom pb-2">
<span
*dsContextHelp="{
content: 'admin.access-control.groups.form.tooltip.editGroupPage',
id: 'edit-group-page',
iconPlacement: 'right',
tooltipPlacement: ['right', 'bottom']
}"
>
{{messagePrefix + '.head.edit' | translate}}
</span>
</h2>
</ng-template>
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"

View File

@@ -269,6 +269,43 @@ describe('GroupFormComponent', () => {
fixture.detectChanges();
});
it('should edit with name and description operations', () => {
const operations = [{
op: 'add',
path: '/metadata/dc.description',
value: 'testDescription'
}, {
op: 'replace',
path: '/name',
value: 'newGroupName'
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
it('should edit with description operations', () => {
component.groupName.value = null;
component.onSubmit();
fixture.detectChanges();
const operations = [{
op: 'add',
path: '/metadata/dc.description',
value: 'testDescription'
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
it('should edit with name operations', () => {
component.groupDescription.value = null;
component.onSubmit();
fixture.detectChanges();
const operations = [{
op: 'replace',
path: '/name',
value: 'newGroupName'
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
it('should emit the existing group using the correct new values', (async () => {
await fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);

View File

@@ -47,6 +47,7 @@ import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { environment } from '../../../../environments/environment';
@Component({
selector: 'ds-group-form',
@@ -198,6 +199,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
label: groupDescription,
name: 'groupDescription',
required: false,
spellCheck: environment.form.spellCheck,
});
this.formModel = [
this.groupName,
@@ -348,8 +350,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
if (hasValue(this.groupDescription.value)) {
operations = [...operations, {
op: 'replace',
path: '/metadata/dc.description/0/value',
op: 'add',
path: '/metadata/dc.description',
value: this.groupDescription.value
}];
}

View File

@@ -1,9 +1,19 @@
<ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
<h4 id="search" class="border-bottom pb-2">
<span
*dsContextHelp="{
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
id: 'edit-group-add-epeople',
iconPlacement: 'right',
tooltipPlacement: ['top', 'right', 'bottom']
}"
>
{{messagePrefix + '.search.head' | translate}}
</span>
</h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div>
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
@@ -59,18 +69,20 @@
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button *ngIf="(ePerson.memberOfGroup)"
<button *ngIf="ePerson.memberOfGroup"
(click)="deleteMemberFromGroup(ePerson)"
class="btn btn-outline-danger btn-sm"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!(ePerson.memberOfGroup)"
<button *ngIf="!ePerson.memberOfGroup"
(click)="addMemberToGroup(ePerson)"
class="btn btn-outline-primary btn-sm"
[disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i class="fas fa-plus fa-fw"></i>
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
@@ -121,10 +133,19 @@
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(ePerson)"
class="btn btn-outline-danger btn-sm"
<button *ngIf="ePerson.memberOfGroup"
(click)="deleteMemberFromGroup(ePerson)"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!ePerson.memberOfGroup"
(click)="addMemberToGroup(ePerson)"
[disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
@@ -39,10 +39,10 @@ describe('MembersListComponent', () => {
let ePersonDataServiceStub: any;
let groupsDataServiceStub: any;
let activeGroup;
let allEPersons;
let allGroups;
let epersonMembers;
let subgroupMembers;
let allEPersons: EPerson[];
let allGroups: Group[];
let epersonMembers: EPerson[];
let subgroupMembers: Group[];
let paginationService;
beforeEach(waitForAsync(() => {
@@ -55,7 +55,7 @@ describe('MembersListComponent', () => {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
findListByHref(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>>> {
@@ -150,8 +150,10 @@ describe('MembersListComponent', () => {
});
afterEach(fakeAsync(() => {
fixture.destroy();
fixture.debugElement.nativeElement.remove();
flush();
component = null;
fixture.debugElement.nativeElement.remove();
}));
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
@@ -170,12 +172,19 @@ describe('MembersListComponent', () => {
describe('search', () => {
describe('when searching without query', () => {
let epersonsFound;
let epersonsFound: DebugElement[];
beforeEach(fakeAsync(() => {
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
return observableOf(activeGroup.epersons.includes(ePerson));
});
component.search({ scope: 'metadata', query: '' });
tick();
fixture.detectChanges();
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
// because they don't change the value of activeGroup.epersons)
jasmine.getEnv().allowRespy(true);
spyOn(component, 'isMemberOfGroup').and.callThrough();
}));
it('should display all epersons', () => {
@@ -184,62 +193,56 @@ describe('MembersListComponent', () => {
describe('if eperson is already a eperson', () => {
it('should have delete button, else it should have add button', () => {
activeGroup.epersons.map((eperson: EPerson) => {
epersonsFound.map((foundEPersonRowElement) => {
if (foundEPersonRowElement.debugElement !== undefined) {
const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child'));
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
if (epersonId.nativeElement.textContent === eperson.id) {
expect(addButton).toBeUndefined();
expect(deleteButton).toBeDefined();
} else {
expect(deleteButton).toBeUndefined();
expect(addButton).toBeDefined();
}
}
});
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
if (memberIds.includes(epersonId.nativeElement.textContent)) {
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
} else {
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
}
});
});
});
describe('if first add button is pressed', () => {
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
addButton.nativeElement.click();
tick();
fixture.detectChanges();
}));
it('all groups in search member of selected group', () => {
it('then all the ePersons are member of the active group', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(2);
epersonsFound.map((foundEPersonRowElement) => {
if (foundEPersonRowElement.debugElement !== undefined) {
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeUndefined();
expect(deleteButton).toBeDefined();
}
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
});
});
});
describe('if first delete button is pressed', () => {
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
addButton.nativeElement.click();
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
deleteButton.nativeElement.click();
tick();
fixture.detectChanges();
}));
it('first eperson in search delete button, because now member', () => {
it('then no ePerson is member of the active group', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
epersonsFound.map((foundEPersonRowElement) => {
if (foundEPersonRowElement.debugElement !== undefined) {
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
expect(deleteButton).toBeUndefined();
expect(addButton).toBeDefined();
}
expect(epersonsFound.length).toEqual(2);
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
});
});
});

View File

@@ -38,6 +38,35 @@ enum SubKey {
SearchResultsDTO,
}
/**
* The layout config of the buttons in the last column
*/
export interface EPersonActionConfig {
/**
* The css classes that should be added to the button
*/
css?: string;
/**
* Whether the button should be disabled
*/
disabled: boolean;
/**
* The Font Awesome icon that should be used
*/
icon: string;
}
/**
* The {@link EPersonActionConfig} that should be used to display the button. The remove config will be used when the
* {@link EPerson} is already a member of the {@link Group} and the remove config will be used otherwise.
*
* *See {@link actionConfig} for an example*
*/
export interface EPersonListActionConfig {
add: EPersonActionConfig;
remove: EPersonActionConfig;
}
@Component({
selector: 'ds-members-list',
templateUrl: './members-list.component.html'
@@ -50,6 +79,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
@Input()
messagePrefix: string;
@Input()
actionConfig: EPersonListActionConfig = {
add: {
css: 'btn-outline-primary',
disabled: false,
icon: 'fas fa-plus fa-fw',
},
remove: {
css: 'btn-outline-danger',
disabled: false,
icon: 'fas fa-trash-alt fa-fw'
},
};
/**
* EPeople being displayed in search result, initially all members, after search result of search
*/
@@ -94,23 +137,21 @@ export class MembersListComponent implements OnInit, OnDestroy {
// current active group being edited
groupBeingEdited: Group;
paginationSub: Subscription;
constructor(private groupDataService: GroupDataService,
public ePersonDataService: EPersonDataService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private formBuilder: FormBuilder,
private paginationService: PaginationService,
private router: Router,
public dsoNameService: DSONameService,
constructor(
protected groupDataService: GroupDataService,
public ePersonDataService: EPersonDataService,
protected translateService: TranslateService,
protected notificationsService: NotificationsService,
protected formBuilder: FormBuilder,
protected paginationService: PaginationService,
protected router: Router,
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
}
ngOnInit() {
ngOnInit(): void {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
@@ -129,7 +170,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @param page the number of the page to retrieve
* @private
*/
private retrieveMembers(page: number) {
retrieveMembers(page: number): void {
this.unsubFrom(SubKey.MembersDTO);
this.subs.set(SubKey.MembersDTO,
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
@@ -140,36 +181,36 @@ export class MembersListComponent implements OnInit, OnDestroy {
}
);
}),
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
} else {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
} else {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
}));
}
/**
* Whether or not the given ePerson is a member of the group currently being edited
* Whether the given ePerson is a member of the group currently being edited
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
@@ -198,7 +239,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @param key The key of the subscription to unsubscribe from
* @private
*/
private unsubFrom(key: SubKey) {
protected unsubFrom(key: SubKey) {
if (this.subs.has(key)) {
this.subs.get(key).unsubscribe();
this.subs.delete(key);
@@ -210,6 +251,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @param ePerson EPerson we want to delete as member from group that is currently being edited
*/
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
ePerson.memberOfGroup = false;
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) {
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
@@ -272,7 +314,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
} else {
return rd;
}

View File

@@ -1,7 +1,16 @@
<ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
<h4 id="search" class="border-bottom pb-2">
<span *dsContextHelp="{
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
id: 'edit-group-add-subgroups',
iconPlacement: 'right',
tooltipPlacement: ['top', 'right', 'bottom']
}"
>
{{messagePrefix + '.search.head' | translate}}
</span>
</h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
@@ -60,7 +69,7 @@
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<p *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</p>
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
(click)="addSubgroupToGroup(group)"

View File

@@ -1,14 +1,6 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
inject,
TestBed,
tick,
waitForAsync
} from '@angular/core/testing';
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
@@ -48,8 +40,8 @@ describe('SubgroupsListComponent', () => {
let ePersonDataServiceStub: any;
let groupsDataServiceStub: any;
let activeGroup;
let subgroups;
let allGroups;
let subgroups: Group[];
let allGroups: Group[];
let routerStub;
let paginationService;
@@ -67,7 +59,7 @@ describe('SubgroupsListComponent', () => {
getSubgroups(): Group {
return this.activeGroup;
},
findListByHref(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));
@@ -136,6 +128,7 @@ describe('SubgroupsListComponent', () => {
});
afterEach(fakeAsync(() => {
fixture.destroy();
fixture.debugElement.nativeElement.remove();
flush();
component = null;
}));
@@ -155,7 +148,7 @@ describe('SubgroupsListComponent', () => {
});
describe('if first group delete button is pressed', () => {
let groupsFound;
let groupsFound: DebugElement[];
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
addButton.triggerEventHandler('click', {
@@ -173,7 +166,7 @@ describe('SubgroupsListComponent', () => {
describe('search', () => {
describe('when searching with empty query', () => {
let groupsFound;
let groupsFound: DebugElement[];
beforeEach(fakeAsync(() => {
component.search({ query: '' });
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
@@ -184,9 +177,9 @@ describe('SubgroupsListComponent', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
expect(groupsFound.length).toEqual(2);
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
allGroups.map((group: Group) => {
expect(groupIdsFound.find((foundEl) => {
expect(groupIdsFound.find((foundEl: DebugElement) => {
return (foundEl.nativeElement.textContent.trim() === group.uuid);
})).toBeTruthy();
});
@@ -198,30 +191,30 @@ describe('SubgroupsListComponent', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
if (getSubgroups !== undefined && getSubgroups.length > 0) {
groupsFound.map((foundGroupRowElement) => {
if (foundGroupRowElement.debugElement !== undefined) {
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeUndefined();
expect(deleteButton).toBeDefined();
groupsFound.map((foundGroupRowElement: DebugElement) => {
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
if (activeGroup.id === groupId.nativeElement.textContent) {
expect(deleteButton).toBeNull();
} else {
expect(deleteButton).not.toBeNull();
}
});
} else {
getSubgroups.map((group: Group) => {
groupsFound.map((foundGroupRowElement) => {
if (foundGroupRowElement.debugElement !== undefined) {
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
if (groupId.nativeElement.textContent === group.id) {
expect(addButton).toBeUndefined();
expect(deleteButton).toBeDefined();
} else {
expect(deleteButton).toBeUndefined();
expect(addButton).toBeDefined();
}
}
});
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
groupsFound.map((foundGroupRowElement: DebugElement) => {
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
} else {
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
}
});
}
});

View File

@@ -7,7 +7,7 @@
<button class="mr-auto btn btn-success"
[routerLink]="['newGroup']">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
</button>
</div>
</div>
@@ -17,7 +17,7 @@
<div class="flex-grow-1 mr-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" attr.aria-label="{{messagePrefix + 'search.placeholder' | translate}}"
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
<span class="input-group-append">
<button type="submit" class="search-button btn btn-primary">

View File

@@ -1,12 +1,8 @@
import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';

View File

@@ -13,32 +13,34 @@
[paginationOptions]="pageConfig"
[pageInfoState]="(bitstreamFormats | async)?.payload"
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
[hideGear]="true"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
</tr>
<tr>
<th scope="col"></th>
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
<td>
<label>
<input type="checkbox"
[checked]="isSelected(bitstreamFormat) | async"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
</tr>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
<td>
<label class="mb-0">
<input type="checkbox"
[checked]="isSelected(bitstreamFormat) | async"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
</tr>
</tbody>
</table>
</div>

View File

@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
});
it('should contain the correct formats', () => {
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(unknownName.textContent).toBe('Unknown');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
expect(UUID.textContent).toBe('test-uuid-1');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(licenseName.textContent).toBe('License');
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
expect(ccLicenseName.textContent).toBe('CC License');
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
expect(adobeName.textContent).toBe('Adobe PDF');
});
});

View File

@@ -1,12 +1,11 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
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 { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
@@ -29,21 +28,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
*/
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/**
* The current pagination configuration for the page used by the FindAll method
* Currently simply renders all bitstream formats
*/
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
/**
* The current pagination configuration for the page
* Currently simply renders all bitstream formats
*/
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'rbp',
pageSize: 20
pageSize: 20,
pageSizeOptions: [20, 40, 60, 80, 100]
});
constructor(private notificationsService: NotificationsService,
@@ -51,7 +43,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
private bitstreamFormatService: BitstreamFormatDataService,
private paginationService: PaginationService,
) {
) {
}
@@ -149,7 +141,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
switchMap((findListOptions: FindListOptions) => {
return this.bitstreamFormatService.findAll(findListOptions);
})

View File

@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { TranslateService } from '@ngx-translate/core';
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
import { environment } from '../../../../../environments/environment';
/**
* The component responsible for rendering the form to create/edit a bitstream format
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
name: 'description',
label: 'admin.registries.bitstream-formats.edit.description.label',
hint: 'admin.registries.bitstream-formats.edit.description.hint',
spellCheck: environment.form.spellCheck,
}),
new DynamicSelectModel({

View File

@@ -29,7 +29,7 @@
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
[ngClass]="{'table-primary' : isActive(schema) | async}">
<td>
<label>
<label class="mb-0">
<input type="checkbox"
[checked]="isSelected(schema) | async"
(change)="selectMetadataSchema(schema, $event)"

View File

@@ -1,3 +1,7 @@
.selectable-row:hover {
cursor: pointer;
}
:host ::ng-deep #metadatadataschemagroup {
display: flex;
}

View File

@@ -19,10 +19,7 @@ import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
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 { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent;

View File

@@ -25,6 +25,7 @@
<thead>
<tr>
<th></th>
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
</tr>
@@ -33,13 +34,14 @@
<tr *ngFor="let field of fields?.page"
[ngClass]="{'table-primary' : isActive(field) | async}">
<td>
<label>
<label class="mb-0">
<input type="checkbox"
[checked]="isSelected(field) | async"
(change)="selectMetadataField(field, $event)">
</label>
</td>
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
</tr>
</tbody>

View File

@@ -1,3 +1,8 @@
.selectable-row:hover {
cursor: pointer;
}
:host ::ng-deep #metadatadatafieldgroup {
display: flex;
flex-wrap: wrap;
}

View File

@@ -23,11 +23,8 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
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 { 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;
@@ -169,10 +166,10 @@ describe('MetadataSchemaComponent', () => {
});
it('should contain the correct fields', () => {
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(editorField.textContent).toBe('mock.contributor.editor');
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
});

View File

@@ -47,6 +47,12 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
},
{
path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
},
])
],
providers: [

View File

@@ -1,7 +1,7 @@
<div class="sidebar-section">
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
[ngClass]="{ disabled: !hasLink }"
[attr.aria-disabled]="!hasLink"
[ngClass]="{ disabled: isDisabled }"
[attr.aria-disabled]="isDisabled"
[attr.aria-labelledby]="'sidebarName-' + section.id"
[title]="('menu.section.icon.' + section.id) | translate"
[routerLink]="itemModel.link"

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
import { Component } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -17,38 +17,86 @@ describe('AdminSidebarSectionComponent', () => {
const menuService = new MenuServiceStub();
const iconString = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
describe('when not disabled', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com'}, icon: iconString}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
it('should not contain the disabled class', () => {
const disabled = fixture.debugElement.query(By.css('.disabled'));
expect(disabled).toBeFalsy();
});
});
describe('when disabled', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com', disabled: true}, icon: iconString}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
it('should contain the disabled class', () => {
const disabled = fixture.debugElement.query(By.css('.disabled'));
expect(disabled).toBeTruthy();
});
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
});
// declare a test component

View File

@@ -5,7 +5,7 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../../shared/menu/menu-section.model';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { isNotEmpty } from '../../../shared/empty.util';
import { isEmpty } from '../../../shared/empty.util';
import { Router } from '@angular/router';
/**
@@ -26,7 +26,12 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
*/
menuID: MenuID = MenuID.ADMIN;
itemModel;
hasLink: boolean;
/**
* Boolean to indicate whether this section is disabled
*/
isDisabled: boolean;
constructor(
@Inject('sectionDataProvider') menuSection: MenuSection,
protected menuService: MenuService,
@@ -38,13 +43,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
}
ngOnInit(): void {
this.hasLink = isNotEmpty(this.itemModel?.link);
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
super.ngOnInit();
}
navigate(event: any): void {
event.preventDefault();
if (this.hasLink) {
if (!this.isDisabled) {
this.router.navigate(this.itemModel.link);
}
}

View File

@@ -6,7 +6,7 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service
import { AdminSidebarComponent } from './admin-sidebar.component';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
import { AuthService } from '../../core/auth/auth.service';
@@ -16,7 +16,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
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';

View File

@@ -5,7 +5,7 @@ import { AuthService } from '../../core/auth/auth.service';
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 { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../../shared/menu/menu-id.model';
import { ActivatedRoute } from '@angular/router';
@@ -69,7 +69,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/
ngOnInit(): void {
super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
this.authService.isAuthenticated()
.subscribe((loggedIn: boolean) => {
if (loggedIn) {
@@ -100,6 +100,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
}
}
});
this.menuVisible = this.menuService.isMenuVisibleWithVisibleSections(this.menuID);
}
@HostListener('focusin')

View File

@@ -7,6 +7,7 @@
[attr.aria-labelledby]="'sidebarName-' + section.id"
[attr.aria-expanded]="expanded | async"
[title]="('menu.section.icon.' + section.id) | translate"
[class.disabled]="section.model?.disabled"
(click)="toggleSection($event)"
(keyup.space)="toggleSection($event)"
(keyup.enter)="toggleSection($event)"

View File

@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
import { of as observableOf } from 'rxjs';
import { Component } from '@angular/core';
@@ -23,7 +23,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString } },
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },

View File

@@ -2,7 +2,7 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
import { rotate } from '../../../shared/animations/rotate';
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
import { slide } from '../../../shared/animations/slide';
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { bgColor } from '../../../shared/animations/bgColor';
import { MenuService } from '../../../shared/menu/menu.service';
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
@@ -65,7 +65,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/
ngOnInit(): void {
super.ngOnInit();
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)

View File

@@ -1 +1 @@
<ds-configuration-search-page configuration="workflowAdmin" [context]="context"></ds-configuration-search-page>
<ds-configuration-search-page configuration="supervision" [context]="context"></ds-configuration-search-page>

View File

@@ -4,24 +4,32 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import {
getWorkflowItemDeleteRoute,
getWorkflowItemSendBackRoute
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
import { of } from 'rxjs';
import { Item } from '../../../../../core/shared/item.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
let component: WorkflowItemAdminWorkflowActionsComponent;
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
let id;
let wfi;
let item = new Item();
item.uuid = 'itemUUID1111';
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
wfi = new WorkflowItem();
wfi.id = id;
wfi.item = of(rd);
}
beforeEach(waitForAsync(() => {
@@ -59,4 +67,5 @@ describe('WorkflowItemAdminWorkflowActionsComponent', () => {
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackRoute(wfi.id)).toString());
});
});

View File

@@ -1,9 +1,10 @@
import { Component, Input } from '@angular/core';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import {
getWorkflowItemSendBackRoute,
getWorkflowItemDeleteRoute
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
getWorkflowItemDeleteRoute,
getWorkflowItemSendBackRoute
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
@Component({
selector: 'ds-workflow-item-admin-workflow-actions-element',
@@ -11,7 +12,7 @@ import {
templateUrl: './workflow-item-admin-workflow-actions.component.html'
})
/**
* The component for displaying the actions for a list element for an item on the admin workflow search page
* The component for displaying the actions for a list element for a workflow-item on the admin workflow search page
*/
export class WorkflowItemAdminWorkflowActionsComponent {
@@ -21,7 +22,7 @@ export class WorkflowItemAdminWorkflowActionsComponent {
@Input() public wfi: WorkflowItem;
/**
* Whether or not to use small buttons
* Whether to use small buttons or not
*/
@Input() public small: boolean;
@@ -29,7 +30,6 @@ export class WorkflowItemAdminWorkflowActionsComponent {
* Returns the path to the delete page of this workflow item
*/
getDeleteRoute(): string {
return getWorkflowItemDeleteRoute(this.wfi.id);
}
@@ -39,4 +39,5 @@ export class WorkflowItemAdminWorkflowActionsComponent {
getSendBackRoute(): string {
return getWorkflowItemSendBackRoute(this.wfi.id);
}
}

View File

@@ -0,0 +1,44 @@
<div>
<div class="modal-header">{{'supervision-group-selector.header' | translate}}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionOrder">{{'supervision-group-selector.select.type-of-order.label' | translate}}</label>
<select name="supervisionOrder" id="supervisionOrder" class="form-control"
[(ngModel)]="selectedOrderType"
attr.aria-label="{{'supervision-group-selector.select.type-of-order.label' | translate}}">
<option value="EDITOR">{{'supervision-group-selector.select.type-of-order.option.editor' | translate}}</option>
<option value="OBSERVER">{{'supervision-group-selector.select.type-of-order.option.observer' | translate}}</option>
</select>
<ds-error *ngIf="isSubmitted && (!selectedOrderType || selectedOrderType === '')"
message="{{'supervision-group-selector.select.type-of-order.error' | translate}}"></ds-error>
</div>
</div>
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionGroup">{{'supervision-group-selector.select.group.label' | translate}}</label>
<ng-container class="mb-3">
<input id="supervisionGroup" class="form-control" type="text" [value]="selectedGroup ? dsoNameService.getName(selectedGroup) : ''" disabled>
<ds-error *ngIf="isSubmitted && !selectedGroup" message="{{'supervision-group-selector.select.group.error' | translate}}"></ds-error>
</ng-container>
<ds-eperson-group-list [isListOfEPerson]="false"
(select)="updateGroupObjectSelected($event)"></ds-eperson-group-list>
</div>
</div>
<!-- <div class="d-flex flex-row-reverse m-2"> -->
<div class="modal-footer">
<button class="btn btn-outline-secondary"
(click)="close()">
<i class="fas fa-times"></i> {{"supervision-group-selector.button.cancel" | translate}}
</button>
<button class="btn btn-primary save"
(click)="save()">
<i class="fas fa-save"></i> {{"supervision-group-selector.button.save" | translate}}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,70 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { SupervisionOrderGroupSelectorComponent } from './supervision-order-group-selector.component';
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
import { NotificationsService } from '../../../../../../shared/notifications/notifications.service';
import { Group } from '../../../../../../core/eperson/models/group.model';
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
import { of } from 'rxjs';
describe('SupervisionOrderGroupSelectorComponent', () => {
let component: SupervisionOrderGroupSelectorComponent;
let fixture: ComponentFixture<SupervisionOrderGroupSelectorComponent>;
let debugElement: DebugElement;
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const supervisionOrderDataService: any = jasmine.createSpyObj('supervisionOrderDataService', {
create: of(new SupervisionOrder())
});
const selectedOrderType = 'NONE';
const itemUUID = 'itemUUID1234';
const selectedGroup = new Group();
selectedGroup.uuid = 'GroupUUID1234';
const supervisionDataObject = new SupervisionOrder();
supervisionDataObject.ordertype = selectedOrderType;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [SupervisionOrderGroupSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
{ provide: NotificationsService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SupervisionOrderGroupSelectorComponent);
component = fixture.componentInstance;
}));
beforeEach(() => {
component.itemUUID = itemUUID;
component.selectedGroup = selectedGroup;
component.selectedOrderType = selectedOrderType;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create component', () => {
expect(component).toBeTruthy();
});
it('should call create for supervision order', () => {
component.save();
fixture.detectChanges();
expect(supervisionOrderDataService.create).toHaveBeenCalledWith(supervisionDataObject, itemUUID, selectedGroup.uuid, selectedOrderType);
});
});

View File

@@ -0,0 +1,97 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
import { Group } from '../../../../../../core/eperson/models/group.model';
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
import { RemoteData } from '../../../../../../core/data/remote-data';
/**
* Component to wrap a dropdown - for type of order -
* and a list of groups
* inside a modal
* Used to create a new supervision order
*/
@Component({
selector: 'ds-supervision-group-selector',
styleUrls: ['./supervision-order-group-selector.component.scss'],
templateUrl: './supervision-order-group-selector.component.html',
})
export class SupervisionOrderGroupSelectorComponent {
/**
* The item to perform the actions on
*/
itemUUID: string;
/**
* The selected supervision order type
*/
selectedOrderType: string;
/**
* selected group for supervision
*/
selectedGroup: Group;
/**
* boolean flag for the validations
*/
isSubmitted = false;
/**
* Event emitted when a new SupervisionOrder has been created
*/
@Output() create: EventEmitter<SupervisionOrder> = new EventEmitter<SupervisionOrder>();
constructor(
public dsoNameService: DSONameService,
private activeModal: NgbActiveModal,
private supervisionOrderDataService: SupervisionOrderDataService,
protected notificationsService: NotificationsService,
protected translateService: TranslateService,
) { }
/**
* Close the modal
*/
close() {
this.activeModal.close();
}
/**
* Assign the value of group on select
*/
updateGroupObjectSelected(object) {
this.selectedGroup = object;
}
/**
* Save the supervision order
*/
save() {
this.isSubmitted = true;
if (this.selectedOrderType && this.selectedGroup) {
let supervisionDataObject = new SupervisionOrder();
supervisionDataObject.ordertype = this.selectedOrderType;
this.supervisionOrderDataService.create(supervisionDataObject, this.itemUUID, this.selectedGroup.uuid, this.selectedOrderType).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<SupervisionOrder>) => {
if (rd.state === 'Success') {
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.dsoNameService.getName(this.selectedGroup) }));
this.create.emit(rd.payload);
this.close();
} else {
this.notificationsService.error(
this.translateService.get('supervision-group-selector.notification.create.failure.title'),
rd.statusCode === 422 ? this.translateService.get('supervision-group-selector.notification.create.already-existing') : rd.errorMessage);
}
});
}
}
}

View File

@@ -0,0 +1,15 @@
<ng-container *ngVar="(supervisionOrderEntries$ | async) as supervisionOrders">
<div class="item-list-supervision" *ngIf="supervisionOrders?.length > 0">
<div>
<span>{{'workflow-item.search.result.list.element.supervised-by' | translate}} </span>
</div>
<div>
<a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge"
[ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate"
(click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close">
{{ dsoNameService.getName(supervisionOrder.group) }}
<span aria-hidden="true"> ×</span>
</a>
</div>
</div>
</ng-container>

View File

@@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { SupervisionOrderStatusComponent } from './supervision-order-status.component';
import { VarDirective } from '../../../../../../shared/utils/var.directive';
import { TranslateLoaderMock } from '../../../../../../shared/mocks/translate-loader.mock';
import { supervisionOrderListMock } from '../../../../../../shared/testing/supervision-order.mock';
describe('SupervisionOrderStatusComponent', () => {
let component: SupervisionOrderStatusComponent;
let fixture: ComponentFixture<SupervisionOrderStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
NgbTooltipModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})
],
declarations: [ SupervisionOrderStatusComponent, VarDirective ],
schemas: [
NO_ERRORS_SCHEMA
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SupervisionOrderStatusComponent);
component = fixture.componentInstance;
component.supervisionOrderList = supervisionOrderListMock;
component.ngOnChanges( {
supervisionOrderList: new SimpleChange(null, supervisionOrderListMock, true)
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render badges properly', () => {
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
expect(badges.length).toBe(2);
});
it('should emit delete event on click', () => {
spyOn(component.delete, 'emit');
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
badges[0].nativeElement.click();
expect(component.delete.emit).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,88 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { map, mergeMap, reduce } from 'rxjs/operators';
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
import { Group } from '../../../../../../core/eperson/models/group.model';
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
import { isNotEmpty } from '../../../../../../shared/empty.util';
import { RemoteData } from '../../../../../../core/data/remote-data';
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
export interface SupervisionOrderListEntry {
supervisionOrder: SupervisionOrder;
group: Group
}
@Component({
selector: 'ds-supervision-order-status',
templateUrl: './supervision-order-status.component.html',
styleUrls: ['./supervision-order-status.component.scss']
})
export class SupervisionOrderStatusComponent implements OnChanges {
/**
* The list of supervision order object to show
*/
@Input() supervisionOrderList: SupervisionOrder[] = [];
/**
* List of the supervision orders combined with the group
*/
supervisionOrderEntries$: BehaviorSubject<SupervisionOrderListEntry[]> = new BehaviorSubject([]);
@Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>();
constructor(
public dsoNameService: DSONameService,
) {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.supervisionOrderList) {
this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue)
.subscribe((supervisionOrderEntries: SupervisionOrderListEntry[]) => {
this.supervisionOrderEntries$.next(supervisionOrderEntries);
});
}
}
/**
* Create a list of SupervisionOrderListEntry by the given SupervisionOrder list
*
* @param supervisionOrderList
*/
private getSupervisionOrderEntries(supervisionOrderList: SupervisionOrder[]): Observable<SupervisionOrderListEntry[]> {
return from(supervisionOrderList).pipe(
mergeMap((so: SupervisionOrder) => so.group.pipe(
getFirstCompletedRemoteData(),
map((sogRD: RemoteData<Group>) => {
if (sogRD.hasSucceeded) {
const entry: SupervisionOrderListEntry = {
supervisionOrder: so,
group: sogRD.payload
};
return entry;
} else {
return null;
}
})
)),
reduce((acc: SupervisionOrderListEntry[], value: any) => {
if (isNotEmpty(value)) {
return [...acc, value];
} else {
return acc;
}
}, []),
);
}
/**
* Emit a delete event with the given SupervisionOrderListEntry.
*/
deleteSupervisionOrder(supervisionOrder: SupervisionOrderListEntry) {
this.delete.emit(supervisionOrder);
}
}

View File

@@ -0,0 +1,16 @@
<div class="my-1">
<ds-supervision-order-status [supervisionOrderList]="supervisionOrderList"
(delete)="deleteSupervisionOrder($event)"></ds-supervision-order-status>
</div>
<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 policies-link" [routerLink]="resourcePoliciesPageRoute" [title]="'admin.workflow.item.policies' | translate">
<i class="fas fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.policies' | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 supervision-group-selector" [title]="'admin.workflow.item.supervision' | translate" (click)="openSupervisionModal()">
<i class="fas fa-users-cog"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.supervision' | translate}}</span>
</a>
</div>

View File

@@ -0,0 +1,156 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import {
getWorkflowItemDeleteRoute,
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
import { Item } from '../../../../../core/shared/item.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
import { supervisionOrderEntryMock } from '../../../../../shared/testing/supervision-order.mock';
import {
SupervisionOrderGroupSelectorComponent
} from './supervision-order-group-selector/supervision-order-group-selector.component';
describe('WorkspaceItemAdminWorkflowActionsComponent', () => {
let component: WorkspaceItemAdminWorkflowActionsComponent;
let fixture: ComponentFixture<WorkspaceItemAdminWorkflowActionsComponent>;
let id;
let wsi;
let item = new Item();
item.uuid = 'itemUUID1111';
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
let supervisionOrderDataService;
let notificationService: NotificationsServiceStub;
function init() {
notificationService = new NotificationsServiceStub();
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
searchByItem: jasmine.createSpy('searchByItem'),
delete: jasmine.createSpy('delete'),
});
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
wsi = new WorkspaceItem();
wsi.id = id;
wsi.item = of(rd);
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
imports: [
NgbModalModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [WorkspaceItemAdminWorkflowActionsComponent],
providers: [
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: NotificationsService, useValue: notificationService },
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WorkspaceItemAdminWorkflowActionsComponent);
component = fixture.componentInstance;
component.wsi = wsi;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render a delete button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.delete-link'));
const link = button.nativeElement.href;
expect(link).toContain(new URLCombiner(getWorkflowItemDeleteRoute(wsi.id)).toString());
});
it('should render a policies button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.policies-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner('/items/itemUUID1111/edit/authorizations').toString());
});
describe('deleteSupervisionOrder', () => {
beforeEach(() => {
spyOn(component.delete, 'emit');
spyOn((component as any).modalService, 'open').and.returnValue({
componentInstance: { response: of(true) }
});
});
describe('when delete succeeded', () => {
beforeEach(() => {
supervisionOrderDataService.delete.and.returnValue(of(true));
});
it('should notify success', () => {
component.deleteSupervisionOrder(supervisionOrderEntryMock);
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
expect(notificationService.success).toHaveBeenCalled();
expect(component.delete.emit).toHaveBeenCalled();
});
});
describe('when delete failed', () => {
beforeEach(() => {
supervisionOrderDataService.delete.and.returnValue(of(false));
});
it('should notify success', () => {
component.deleteSupervisionOrder(supervisionOrderEntryMock);
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
expect(notificationService.error).toHaveBeenCalled();
expect(component.delete.emit).not.toHaveBeenCalled();
});
});
});
describe('openSupervisionModal', () => {
beforeEach(() => {
spyOn(component.create, 'emit');
spyOn((component as any).modalService, 'open').and.returnValue({
componentInstance: { create: of(true) }
});
});
it('should emit create event properly', () => {
component.openSupervisionModal();
expect((component as any).modalService.open).toHaveBeenCalledWith(SupervisionOrderGroupSelectorComponent, {
size: 'lg',
backdrop: 'static'
});
expect(component.create.emit).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,192 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { map, Observable } from 'rxjs';
import { switchMap, take, tap } from 'rxjs/operators';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { Item } from '../../../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
import {
SupervisionOrderGroupSelectorComponent
} from './supervision-order-group-selector/supervision-order-group-selector.component';
import {
getWorkflowItemDeleteRoute
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
import { SupervisionOrderListEntry } from './supervision-order-status/supervision-order-status.component';
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../../../../shared/empty.util';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
import { getSearchResultFor } from '../../../../../shared/search/search-result-element-decorator';
@Component({
selector: 'ds-workspace-item-admin-workflow-actions-element',
styleUrls: ['./workspace-item-admin-workflow-actions.component.scss'],
templateUrl: './workspace-item-admin-workflow-actions.component.html'
})
/**
* The component for displaying the actions for a list element for a workspace-item on the admin workflow search page
*/
export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
/**
* The workspace item to perform the actions on
*/
@Input() public wsi: WorkspaceItem;
/**
* Whether to use small buttons or not
*/
@Input() public small: boolean;
/**
* The list of supervision order object to show
*/
@Input() supervisionOrderList: SupervisionOrder[] = [];
/**
* The item related to the workspace item
*/
item: Item;
/**
* An array containing the route to the resource policies page
*/
resourcePoliciesPageRoute: string[];
/**
* The i18n keys prefix
* @private
*/
private messagePrefix = 'workflow-item.search.result';
/**
* Event emitted when a new SupervisionOrder has been created
*/
@Output() create: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
/**
* Event emitted when new SupervisionOrder has been deleted
*/
@Output() delete: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
/**
* Event emitted when a new SupervisionOrder has been created
*/
constructor(
protected dsoNameService: DSONameService,
protected modalService: NgbModal,
protected notificationsService: NotificationsService,
protected supervisionOrderDataService: SupervisionOrderDataService,
protected translateService: TranslateService,
) {
}
ngOnInit(): void {
const item$: Observable<Item> = this.wsi.item.pipe(
getFirstSucceededRemoteDataPayload(),
);
item$.pipe(
map((item: Item) => this.getPoliciesRoute(item))
).subscribe((route: string[]) => {
this.resourcePoliciesPageRoute = route;
});
item$.subscribe((item: Item) => {
this.item = item;
});
}
/**
* Returns the path to the delete page of this workflow item
*/
getDeleteRoute(): string {
return getWorkflowItemDeleteRoute(this.wsi.id);
}
/**
* Returns the path to the administrative edit page policies tab
*/
getPoliciesRoute(item: Item): string[] {
return ['/items', item.uuid, 'edit', ITEM_EDIT_AUTHORIZATIONS_PATH];
}
/**
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
* It'll either show a success or error message depending on whether delete was successful or not.
*/
deleteSupervisionOrder(supervisionOrderEntry: SupervisionOrderListEntry) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = supervisionOrderEntry.group;
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-supervision.modal.header';
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-supervision.modal.info';
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-supervision.modal.cancel';
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-supervision.modal.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(
take(1),
switchMap((confirm: boolean) => {
if (confirm && hasValue(supervisionOrderEntry.supervisionOrder.id)) {
return this.supervisionOrderDataService.delete(supervisionOrderEntry.supervisionOrder.id).pipe(
take(1),
tap((result: boolean) => {
if (result) {
this.notificationsService.success(
null,
this.translateService.get(
this.messagePrefix + '.notification.deleted.success',
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
)
);
} else {
this.notificationsService.error(
null,
this.translateService.get(
this.messagePrefix + '.notification.deleted.failure',
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
)
);
}
})
);
}
})
).subscribe((result: boolean) => {
if (result) {
this.delete.emit(this.convertReloadedObject());
}
});
}
/**
* Opens the Supervision Modal to create a supervision order
*/
openSupervisionModal() {
const supervisionModal: NgbModalRef = this.modalService.open(SupervisionOrderGroupSelectorComponent, {
size: 'lg',
backdrop: 'static'
});
supervisionModal.componentInstance.itemUUID = this.item.uuid;
supervisionModal.componentInstance.create.subscribe(() => {
this.create.emit(this.convertReloadedObject());
});
}
/**
* Convert the reloadedObject to the Type required by this dso.
*/
private convertReloadedObject(): DSpaceObject {
const constructor = getSearchResultFor((this.wsi as any).constructor);
return Object.assign(new constructor(), this.wsi, {
indexableObject: this.wsi
});
}
}

View File

@@ -7,14 +7,22 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { RouterTestingModule } from '@angular/router/testing';
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component';
import {
WorkflowItemSearchResultAdminWorkflowGridElementComponent
} from './workflow-item-search-result-admin-workflow-grid-element.component';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { Item } from '../../../../../core/shared/item.model';
import { ItemGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import {
ItemGridElementComponent
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
@@ -22,7 +30,7 @@ import { of as observableOf } from 'rxjs';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
let id;

View File

@@ -0,0 +1,16 @@
<ng-template dsListableObject>
</ng-template>
<div #badges class="position-absolute ml-1">
<div class="workflow-badge">
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
</div>
</div>
<ul #buttons class="list-group list-group-flush">
<li class="list-group-item">
<ds-workspace-item-admin-workflow-actions-element [small]="true"
[supervisionOrderList]="supervisionOrder$ | async"
[wsi]="dso"
(create)="reloadObject($event)"
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>
</li>
</ul>

View File

@@ -0,0 +1,127 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import {
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
} from './workspace-item-search-result-admin-workflow-grid-element.component';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { Item } from '../../../../../core/shared/item.model';
import {
ItemGridElementComponent
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import {
supervisionOrderPaginatedListRD,
supervisionOrderPaginatedListRD$
} from '../../../../../shared/testing/supervision-order.mock';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
let component: WorkspaceItemSearchResultAdminWorkflowGridElementComponent;
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowGridElementComponent>;
let id;
let wfi;
let itemRD$;
let linkService;
let object;
let themeService;
let supervisionOrderDataService;
function init() {
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
object = new WorkflowItemSearchResult();
wfi = new WorkflowItem();
wfi.item = itemRD$;
object.indexableObject = wfi;
linkService = getMockLinkService();
themeService = getMockThemeService();
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
searchByItem: jasmine.createSpy('searchByItem'),
delete: jasmine.createSpy('delete'),
});
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule(
{
declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]),
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ThemeService, useValue: themeService },
{
provide: TruncatableService, useValue: {
isCollapsed: () => observableOf(true),
}
},
{ provide: BitstreamDataService, useValue: {} },
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
],
schemas: [NO_ERRORS_SCHEMA]
})
.overrideComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent, {
set: {
entryComponents: [ItemGridElementComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
linkService.resolveLink.and.callFake((a) => a);
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent);
component = fixture.componentInstance;
component.object = object;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should retrieve the item using the link service', () => {
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
});
it('should retrieve supervision order objects properly', () => {
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
});
it('should emit reloadedObject properly ', () => {
spyOn(component.reloadedObject, 'emit');
const dso = new DSpaceObject();
component.reloadObject(dso);
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
});
});

View File

@@ -0,0 +1,160 @@
import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { Item } from '../../../../../core/shared/item.model';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import {
getListableObjectComponent,
listableObjectComponent
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import {
SearchResultGridElementComponent
} from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import {
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
getRemoteDataPayload
} from '../../../../../core/shared/operators';
import {
WorkspaceItemSearchResult
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
@Component({
selector: 'ds-workflow-item-search-result-admin-workflow-grid-element',
styleUrls: ['./workspace-item-search-result-admin-workflow-grid-element.component.scss'],
templateUrl: './workspace-item-search-result-admin-workflow-grid-element.component.html'
})
/**
* The component for displaying a grid element for an workflow item on the admin workflow search page
*/
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
/**
* The item linked to the workspace item
*/
public item$: Observable<Item>;
/**
* The id of the item linked to the workflow item
*/
public itemId: string;
/**
* The supervision orders linked to the workflow item
*/
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
/**
* Directive used to render the dynamic component in
*/
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
/**
* The html child that contains the badges html
*/
@ViewChild('badges', { static: true }) badges: ElementRef;
/**
* The html child that contains the button html
*/
@ViewChild('buttons', { static: true }) buttons: ElementRef;
constructor(
public dsoNameService: DSONameService,
private componentFactoryResolver: ComponentFactoryResolver,
private linkService: LinkService,
protected truncatableService: TruncatableService,
private themeService: ThemeService,
protected bitstreamDataService: BitstreamDataService,
protected supervisionOrderDataService: SupervisionOrderDataService,
) {
super(dsoNameService, truncatableService, bitstreamDataService);
}
/**
* Setup the dynamic child component
* Initialize the item object from the workflow item
*/
ngOnInit(): void {
super.ngOnInit();
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
this.item$.pipe(take(1)).subscribe((item: Item) => {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(
componentFactory,
0,
undefined,
[
[this.badges.nativeElement],
[this.buttons.nativeElement]
]);
(componentRef.instance as any).object = item;
(componentRef.instance as any).index = this.index;
(componentRef.instance as any).linkType = this.linkType;
(componentRef.instance as any).listID = this.listID;
componentRef.changeDetectorRef.detectChanges();
}
);
this.item$.pipe(
take(1),
tap((item: Item) => this.itemId = item.id),
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
this.supervisionOrder$.next(supervisionOrderList);
});
}
/**
* Fetch the component depending on the item's entity type, view mode and context
* @returns {GenericConstructor<Component>}
*/
private getComponent(item: Item): GenericConstructor<Component> {
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
}
/**
* Retrieve the list of SupervisionOrder object related to the given item
*
* @param itemId
* @private
*/
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
return this.supervisionOrderDataService.searchByItem(
itemId, false, true, followLink('group')
).pipe(
getFirstCompletedRemoteData(),
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
);
}
reloadObject(dso: DSpaceObject) {
this.reloadedObject.emit(dso);
}
}

View File

@@ -9,11 +9,15 @@ import { CollectionElementLinkType } from '../../../../../shared/object-collecti
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { RouterTestingModule } from '@angular/router/testing';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component';
import {
WorkflowItemSearchResultAdminWorkflowListElementComponent
} from './workflow-item-search-result-admin-workflow-list-element.component';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { Item } from '../../../../../core/shared/item.model';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@@ -21,7 +25,7 @@ import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
let id;

View File

@@ -1,6 +1,8 @@
import { Component, Inject, OnInit } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import {
listableObjectComponent
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { Observable } from 'rxjs';
@@ -9,9 +11,13 @@ import { followLink } from '../../../../../shared/utils/follow-link-config.model
import { RemoteData } from '../../../../../core/data/remote-data';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
import { Item } from '../../../../../core/shared/item.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import {
SearchResultListElementComponent
} from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@@ -22,7 +28,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.inter
templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html'
})
/**
* The component for displaying a list element for an workflow item on the admin workflow search page
* The component for displaying a list element for a workflow item on the admin workflow search page
*/
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {

View File

@@ -0,0 +1,13 @@
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
<ds-listable-object-component-loader *ngIf="item$ | async"
[object]="item$ | async"
[viewMode]="viewModes.ListElement"
[index]="index"
[linkType]="linkType"
[listID]="listID"></ds-listable-object-component-loader>
<ds-workspace-item-admin-workflow-actions-element [small]="false"
[supervisionOrderList]="supervisionOrder$ | async"
[wsi]="dso"
(create)="reloadObject($event)"
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>

View File

@@ -0,0 +1,111 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import {
WorkspaceItemSearchResultAdminWorkflowListElementComponent
} from './workspace-item-search-result-admin-workflow-list-element.component';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { Item } from '../../../../../core/shared/item.model';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import {
supervisionOrderPaginatedListRD,
supervisionOrderPaginatedListRD$
} from '../../../../../shared/testing/supervision-order.mock';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
describe('WorkspaceItemSearchResultAdminWorkflowListElementComponent', () => {
let component: WorkspaceItemSearchResultAdminWorkflowListElementComponent;
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowListElementComponent>;
let id;
let wfi;
let itemRD$;
let linkService;
let object;
let supervisionOrderDataService;
function init() {
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
object = new WorkflowItemSearchResult();
wfi = new WorkflowItem();
wfi.item = itemRD$;
object.indexableObject = wfi;
linkService = getMockLinkService();
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
searchByItem: jasmine.createSpy('searchByItem'),
delete: jasmine.createSpy('delete'),
});
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule(
{
declarations: [WorkspaceItemSearchResultAdminWorkflowListElementComponent],
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]),
],
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
{ provide: APP_CONFIG, useValue: environment }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
linkService.resolveLink.and.callFake((a) => a);
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowListElementComponent);
component = fixture.componentInstance;
component.object = object;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should retrieve the item using the link service', () => {
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
});
it('should retrieve supervision order objects properly', () => {
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
});
it('should emit reloadedObject properly ', () => {
spyOn(component.reloadedObject, 'emit');
const dso = new DSpaceObject();
component.reloadObject(dso);
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
});
});

View File

@@ -0,0 +1,109 @@
import { Component, Inject, OnInit } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import {
listableObjectComponent
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import {
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
getRemoteDataPayload
} from '../../../../../core/shared/operators';
import { Item } from '../../../../../core/shared/item.model';
import {
SearchResultListElementComponent
} from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
import {
WorkspaceItemSearchResult
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
@Component({
selector: 'ds-workflow-item-search-result-admin-workflow-list-element',
styleUrls: ['./workspace-item-search-result-admin-workflow-list-element.component.scss'],
templateUrl: './workspace-item-search-result-admin-workflow-list-element.component.html'
})
/**
* The component for displaying a list element for a workflow item on the admin workflow search page
*/
export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
/**
* The item linked to the workflow item
*/
public item$: Observable<Item>;
/**
* The id of the item linked to the workflow item
*/
public itemId: string;
/**
* The supervision orders linked to the workflow item
*/
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
constructor(private linkService: LinkService,
public dsoNameService: DSONameService,
protected supervisionOrderDataService: SupervisionOrderDataService,
protected truncatableService: TruncatableService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) {
super(truncatableService, dsoNameService, appConfig);
}
/**
* Initialize the item object from the workflow item
*/
ngOnInit(): void {
super.ngOnInit();
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
this.item$.pipe(
take(1),
tap((item: Item) => this.itemId = item.id),
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
this.supervisionOrder$.next(supervisionOrderList);
});
}
/**
* Retrieve the list of SupervisionOrder object related to the given item
*
* @param itemId
* @private
*/
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
return this.supervisionOrderDataService.searchByItem(
itemId, false, true, followLink('group')
).pipe(
getFirstCompletedRemoteData(),
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
);
}
/**
* Reload list element after supervision order change.
*/
reloadObject(dso: DSpaceObject) {
this.reloadedObject.emit(dso);
}
}

View File

@@ -1,16 +1,39 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../../shared/shared.module';
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
import { SharedModule } from '../../shared/shared.module';
import {
WorkflowItemSearchResultAdminWorkflowGridElementComponent
} from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
import {
WorkflowItemAdminWorkflowActionsComponent
} from './admin-workflow-search-results/actions/workflow-item/workflow-item-admin-workflow-actions.component';
import {
WorkflowItemSearchResultAdminWorkflowListElementComponent
} from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
import { SearchModule } from '../../shared/search/search.module';
import {
WorkspaceItemAdminWorkflowActionsComponent
} from './admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component';
import {
WorkspaceItemSearchResultAdminWorkflowListElementComponent
} from './admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component';
import {
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
} from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component';
import {
SupervisionOrderGroupSelectorComponent
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-group-selector/supervision-order-group-selector.component';
import {
SupervisionOrderStatusComponent
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
WorkflowItemSearchResultAdminWorkflowListElementComponent,
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
WorkspaceItemSearchResultAdminWorkflowListElementComponent,
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
];
@NgModule({
@@ -20,7 +43,10 @@ const ENTRY_COMPONENTS = [
],
declarations: [
AdminWorkflowPageComponent,
SupervisionOrderGroupSelectorComponent,
SupervisionOrderStatusComponent,
WorkflowItemAdminWorkflowActionsComponent,
WorkspaceItemAdminWorkflowActionsComponent,
...ENTRY_COMPONENTS
],
exports: [

View File

@@ -10,6 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module';
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -25,7 +26,8 @@ const ENTRY_COMPONENTS = [
AccessControlModule,
AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(),
SharedModule
SharedModule,
UploadModule,
],
declarations: [
AdminCurationTasksComponent,

View File

@@ -126,3 +126,9 @@ export function getRequestCopyModulePath() {
}
export const HEALTH_PAGE_PATH = 'health';
export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';
export function getSubscriptionsModuleRoute() {
return `/${SUBSCRIPTIONS_MODULE_PATH}`;
}

View File

@@ -230,6 +230,12 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
canActivate: [GroupAdministratorGuard],
},
{
path: 'subscriptions',
loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module')
.then((m) => m.SubscriptionsPageRoutingModule),
canActivate: [AuthenticatedGuard]
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
]
}

View File

@@ -18,7 +18,7 @@ import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.ser
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
import { AuthService } from './core/auth/auth.service';
import { MenuService } from './shared/menu/menu.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { HostWindowService } from './shared/host-window.service';

View File

@@ -25,13 +25,12 @@ 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 { CSSVariableService } from './shared/sass-helper/css-variable.service';
import { environment } from '../environments/environment';
import { models } from './core/core.module';
import { ThemeService } from './shared/theme-support/theme.service';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { distinctNext } from './core/shared/distinct-next';
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
@Component({
selector: 'ds-app',
@@ -110,18 +109,8 @@ export class AppComponent implements OnInit, AfterViewInit {
}
private storeCSSVariables() {
this.cssService.addCSSVariable('xlMin', '1200px');
this.cssService.addCSSVariable('mdMin', '768px');
this.cssService.addCSSVariable('lgMin', '576px');
this.cssService.addCSSVariable('smMin', '0');
this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28');
this.cssService.addCSSVariable('sidebarItemsWidth', '250px');
this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px');
this.cssService.addCSSVariable('totalSidebarWidth', '303.234px');
// const vars = variables.locals || {};
// Object.keys(vars).forEach((name: string) => {
// this.cssService.addCSSVariable(name, vars[name]);
// })
this.cssService.clearCSSVariables();
this.cssService.addCSSVariables(this.cssService.getCSSVariablesFromStylesheets(this.document));
}
ngAfterViewInit() {

View File

@@ -1,16 +1,16 @@
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
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, 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 { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { appEffects } from './app.effects';
@@ -28,7 +28,6 @@ import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { LogInterceptor } from './core/log/log.interceptor';
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';
@@ -46,14 +45,6 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
}
/**
* Condition for displaying error messages on email form field
*/
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
};
const IMPORTS = [
CommonModule,
SharedModule,
@@ -64,7 +55,6 @@ const IMPORTS = [
ScrollToModule.forRoot(),
NgbModule,
TranslateModule.forRoot(),
NgxMaskModule.forRoot(),
EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(),
@@ -113,10 +103,7 @@ const PROVIDERS = [
useClass: LogInterceptor,
multi: true
},
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher
},
// register the dynamic matcher used by form. MUST be provided by the app module
...DYNAMIC_MATCHER_PROVIDERS,
];

View File

@@ -1,4 +1,4 @@
import * as fromRouter from '@ngrx/router-store';
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import {
ePeopleRegistryReducer,
@@ -35,31 +35,27 @@ import {
ObjectSelectionListState,
objectSelectionReducer
} from './shared/object-select/object-select.reducer';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/css-variable.reducer';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import {
filterReducer,
SearchFiltersState
} from './shared/search/search-filters/search-filter/search-filter.reducer';
import {
sidebarFilterReducer,
SidebarFiltersState
} from './shared/sidebar/filter/sidebar-filter.reducer';
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';
import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
router: RouterReducerState;
hostWindow: HostWindowState;
forms: FormState;
metadataRegistry: MetadataRegistryState;
notifications: NotificationsState;
sidebar: SidebarState;
sidebarFilter: SidebarFiltersState;
searchFilter: SearchFiltersState;
truncatable: TruncatablesState;
cssVariables: CSSVariablesState;
@@ -72,16 +68,16 @@ export interface AppState {
epeopleRegistry: EPeopleRegistryState;
groupRegistry: GroupRegistryState;
correlationId: string;
contextHelp: ContextHelpState;
}
export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
router: routerReducer,
hostWindow: hostWindowReducer,
forms: formReducer,
metadataRegistry: metadataRegistryReducer,
notifications: notificationsReducer,
sidebar: sidebarReducer,
sidebarFilter: sidebarFilterReducer,
searchFilter: filterReducer,
truncatable: truncatableReducer,
cssVariables: cssVariablesReducer,
@@ -93,7 +89,8 @@ export const appReducers: ActionReducerMap<AppState> = {
communityList: CommunityListReducer,
epeopleRegistry: ePeopleRegistryReducer,
groupRegistry: groupRegistryReducer,
correlationId: correlationIdReducer
correlationId: correlationIdReducer,
contextHelp: contextHelpReducer,
};
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -6,7 +6,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { ActivatedRoute, Router } from '@angular/router';
import { getForbiddenRoute } from '../../app-routing-paths';
import { TranslateModule } from '@ngx-translate/core';

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { hasValue, isNotEmpty } from '../empty.util';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { getRemoteDataPayload} from '../../core/shared/operators';
import { Bitstream } from '../../core/shared/bitstream.model';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';

View File

@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { BitstreamPageResolver } from './bitstream-page.resolver';
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';

View File

@@ -6,6 +6,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
import { FormModule } from '../shared/form/form.module';
import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module';
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
/**
* This module handles all components that are necessary for Bitstream related pages
@@ -20,7 +21,8 @@ import { ResourcePoliciesModule } from '../shared/resource-policies/resource-pol
],
declarations: [
BitstreamAuthorizationsComponent,
EditBitstreamPageComponent
EditBitstreamPageComponent,
BitstreamDownloadPageComponent,
]
})
export class BitstreamPageModule {

View File

@@ -26,7 +26,7 @@ import {
import { FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { cloneDeep } from 'lodash';
import cloneDeep from 'lodash/cloneDeep';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import {
getAllSucceededRemoteDataPayload,

View File

@@ -1,5 +1,5 @@
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
import { of as observableOf, EMPTY } from 'rxjs';
import { EMPTY } from 'rxjs';
import { BitstreamDataService } from '../core/data/bitstream-data.service';
import { RemoteData } from '../core/data/remote-data';
import { TestScheduler } from 'rxjs/testing';

View File

@@ -10,7 +10,7 @@
</nav>
<ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li>
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li>
</ng-template>
<ng-template #activeBreadcrumb let-text="text">

View File

@@ -23,11 +23,14 @@ li.breadcrumb-item {
}
}
li.breadcrumb-item > a {
color: var(--ds-breadcrumb-link-color) !important;
li.breadcrumb-item {
a {
color: var(--ds-breadcrumb-link-color);
}
}
li.breadcrumb-item.active {
color: var(--ds-breadcrumb-link-active-color) !important;
color: var(--ds-breadcrumb-link-active-color);
}
.breadcrumb-item+ .breadcrumb-item::before {

View File

@@ -68,6 +68,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
this.updatePageWithItems(searchOptions, this.value, undefined);
this.updateParent(params.scope);
this.updateLogo();
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
}));
}

View File

@@ -1,10 +1,17 @@
<div class="container">
<ng-container *ngVar="(parent$ | async) as parent">
<ng-container *ngIf="parent?.payload as parentContext">
<header class="comcol-header border-bottom mb-4 pb-4">
<div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto">
<!-- Parent Name -->
<ds-comcol-page-header [name]="dsoNameService.getName(parentContext)">
</ds-comcol-page-header>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logo$"
[logo]="(logo$ | async)?.payload"
[alternateText]="'Community or Collection Logo'">
</ds-comcol-page-logo>
<!-- Handle -->
<ds-themed-comcol-page-handle
[content]="parentContext.handle"
@@ -17,6 +24,8 @@
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<!-- Browse-By Links -->
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
</ng-container></ng-container>

View File

@@ -144,6 +144,9 @@ describe('BrowseByMetadataPageComponent', () => {
route.params = observableOf(paramsWithValue);
comp.ngOnInit();
comp.updateParent('fake-scope');
comp.updateLogo();
fixture.detectChanges();
});
it('should fetch items', () => {
@@ -151,6 +154,10 @@ describe('BrowseByMetadataPageComponent', () => {
expect(result.payload.page).toEqual(mockItems);
});
});
it('should fetch the logo', () => {
expect(comp.logo$).toBeTruthy();
});
});
describe('when calling browseParamsToOptions', () => {

View File

@@ -15,7 +15,11 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
import { filter, map, mergeMap } from 'rxjs/operators';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -49,6 +53,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
parent$: Observable<RemoteData<DSpaceObject>>;
/**
* The logo of the current Community or Collection
*/
logo$: Observable<RemoteData<Bitstream>>;
/**
* The pagination config used to display the values
*/
@@ -154,6 +163,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
}
this.updateParent(params.scope);
this.updateLogo();
}));
this.updateStartsWithTextOptions();
@@ -199,12 +209,31 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
updateParent(scope: string) {
if (hasValue(scope)) {
this.parent$ = this.dsoService.findById(scope).pipe(
const linksToFollow = () => {
return [followLink('logo')];
};
this.parent$ = this.dsoService.findById(scope,
true,
true,
...linksToFollow() as FollowLinkConfig<DSpaceObject>[]).pipe(
getFirstSucceededRemoteData()
);
}
}
/**
* Update the parent Community or Collection logo
*/
updateLogo() {
if (hasValue(this.parent$)) {
this.logo$ = this.parent$.pipe(
map((rd: RemoteData<Collection | Community>) => rd.payload),
filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)),
mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo)
);
}
}
/**
* Navigate to the previous page
*/

View File

@@ -4,16 +4,21 @@ import { BrowseByModule } from './browse-by.module';
import { ItemDataService } from '../core/data/item-data.service';
import { BrowseService } from '../core/browse/browse.service';
import { BrowseByGuard } from './browse-by-guard';
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
@NgModule({
imports: [
SharedBrowseByModule,
BrowseByRoutingModule,
BrowseByModule.withEntryComponents()
BrowseByModule.withEntryComponents(),
],
providers: [
ItemDataService,
BrowseService,
BrowseByGuard
BrowseByGuard,
],
declarations: [
]
})
export class BrowseByPageModule {

View File

@@ -4,13 +4,17 @@ import { BrowseByGuard } from './browse-by-guard';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
resolve: {
breadcrumb: BrowseByDSOBreadcrumbResolver,
menu: DSOEditMenuResolver
},
children: [
{
path: ':id',

View File

@@ -52,6 +52,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
this.updateParent(params.scope);
this.updateLogo();
}));
this.updateStartsWithTextOptions();
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
import { SharedModule } from '../shared/shared.module';
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
@@ -10,6 +9,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -25,9 +26,10 @@ const ENTRY_COMPONENTS = [
@NgModule({
imports: [
SharedBrowseByModule,
CommonModule,
ComcolModule,
SharedModule
DsoPageModule
],
declarations: [
BrowseBySwitcherComponent,
@@ -45,7 +47,7 @@ export class BrowseByModule {
*/
static withEntryComponents() {
return {
ngModule: SharedModule,
ngModule: SharedBrowseByModule,
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
};
}

View File

@@ -1,5 +1,6 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
import { environment } from '../../../environments/environment';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'entityType',
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'license',
name: 'dc.rights.license',
spellCheck: environment.form.spellCheck,
})
];

View File

@@ -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 { ChangeDetectionStrategy, EventEmitter } from '@angular/core';
import { 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,7 +41,7 @@ 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 { 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';

View File

@@ -21,6 +21,7 @@ import { CollectionPageAdministratorGuard } from './collection-page-administrato
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';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -34,7 +35,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
path: ':id',
resolve: {
dso: CollectionPageResolver,
breadcrumb: CollectionBreadcrumbResolver
breadcrumb: CollectionBreadcrumbResolver,
menu: DSOEditMenuResolver
},
runGuardsAndResolvers: 'always',
children: [
@@ -72,6 +74,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_collection_:id',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',
@@ -90,7 +93,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
DSOBreadcrumbsService,
LinkService,
CreateCollectionPageGuard,
CollectionPageAdministratorGuard
CollectionPageAdministratorGuard,
]
})
export class CollectionPageRoutingModule {

View File

@@ -33,8 +33,9 @@
[title]="'collection.page.news'">
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
<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>
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
</div>
</div>
<section class="comcol-page-browse-section">

View File

@@ -16,6 +16,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
import { CollectionFormModule } from './collection-form/collection-form.module';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { ComcolModule } from '../shared/comcol/comcol.module';
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
@NgModule({
imports: [
@@ -25,7 +27,9 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
StatisticsModule.forRoot(),
EditItemPageModule,
CollectionFormModule,
ComcolModule
ComcolModule,
DsoSharedModule,
DsoPageModule,
],
declarations: [
CollectionPageComponent,
@@ -38,7 +42,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
],
providers: [
SearchService,
]
],
})
export class CollectionPageModule {

View File

@@ -6,6 +6,7 @@ import { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { HALLink } from '../../../core/shared/hal-link.model';
import { hasValue } from '../../../shared/empty.util';
/**
* Component for managing a collection's roles
@@ -45,25 +46,31 @@ export class CollectionRolesComponent implements OnInit {
);
this.comcolRoles$ = this.collection$.pipe(
map((collection) => [
{
name: 'collection-admin',
href: collection._links.adminGroup.href,
},
{
name: 'submitters',
href: collection._links.submittersGroup.href,
},
{
name: 'item_read',
href: collection._links.itemReadGroup.href,
},
{
name: 'bitstream_read',
href: collection._links.bitstreamReadGroup.href,
},
...collection._links.workflowGroups,
]),
map((collection) => {
let workflowGroups: HALLink[] | HALLink = hasValue(collection._links.workflowGroups) ? collection._links.workflowGroups : [];
if (!Array.isArray(workflowGroups)) {
workflowGroups = [workflowGroups];
}
return [
{
name: 'collection-admin',
href: collection._links.adminGroup.href,
},
{
name: 'submitters',
href: collection._links.submittersGroup.href,
},
{
name: 'item_read',
href: collection._links.itemReadGroup.href,
},
{
name: 'bitstream_read',
href: collection._links.bitstreamReadGroup.href,
},
...workflowGroups,
];
}),
);
}
}

View File

@@ -11,11 +11,11 @@
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
<span>{{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
<span>{{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"

View File

@@ -8,8 +8,7 @@ import {
DynamicInputModel,
DynamicOptionControlModel,
DynamicRadioGroupModel,
DynamicSelectModel,
DynamicTextAreaModel
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
@@ -23,7 +22,7 @@ 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 { cloneDeep } from 'lodash';
import cloneDeep from 'lodash/cloneDeep';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { MetadataConfig } from '../../../core/shared/metadata-config.model';

View File

@@ -25,7 +25,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
CollectionFormModule,
ResourcePoliciesModule,
FormModule,
ComcolModule
ComcolModule,
],
declarations: [
EditCollectionPageComponent,

View File

@@ -3,7 +3,7 @@
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
<ng-container *ngIf="itemRD?.hasSucceeded">
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}</h2>
<ds-themed-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-themed-item-metadata>
<ds-themed-dso-edit-metadata [updateDataService]="itemTemplateService" [dso]="itemRD?.payload"></ds-themed-dso-edit-metadata>
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
</ng-container>
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>

View File

@@ -6,6 +6,7 @@ import { CommunityListPageRoutingModule } from './community-list-page.routing.mo
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';
import { CdkTreeModule } from '@angular/cdk/tree';
const DECLARATIONS = [
@@ -21,13 +22,15 @@ const DECLARATIONS = [
imports: [
CommonModule,
SharedModule,
CommunityListPageRoutingModule
CommunityListPageRoutingModule,
CdkTreeModule,
],
declarations: [
...DECLARATIONS
],
exports: [
...DECLARATIONS,
CdkTreeModule,
],
})
export class CommunityListPageModule {

View File

@@ -13,6 +13,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 { environment } from '../../../environments/environment';
/**
* Form used for creating and editing communities
@@ -52,18 +53,22 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}),
];

View File

@@ -14,6 +14,7 @@ import { CommunityPageAdministratorGuard } from './community-page-administrator.
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';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -27,7 +28,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
path: ':id',
resolve: {
dso: CommunityPageResolver,
breadcrumb: CommunityBreadcrumbResolver
breadcrumb: CommunityBreadcrumbResolver,
menu: DSOEditMenuResolver
},
runGuardsAndResolvers: 'always',
children: [
@@ -55,6 +57,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_community_:id',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',
@@ -72,7 +75,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
DSOBreadcrumbsService,
LinkService,
CreateCommunityPageGuard,
CommunityPageAdministratorGuard
CommunityPageAdministratorGuard,
]
})
export class CommunityPageRoutingModule {

Some files were not shown because too many files have changed in this diff Show More