Merge branch 'DSpace:main' into ul-and-li-in-dropdwon-collection-in-submissions-form

This commit is contained in:
Cristian Emanuelle Guzmán Suárez
2023-02-28 20:47:36 -06:00
committed by GitHub
448 changed files with 12204 additions and 1956 deletions

View File

@@ -369,3 +369,8 @@ vocabularies:
- filter: 'subject'
vocabulary: 'srsc'
enabled: true
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'

View File

@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
"version": "7.5.0-next",
"version": "7.6.0-next",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",

View File

@@ -37,7 +37,6 @@ import { json } from 'body-parser';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
@@ -65,7 +64,7 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
// Set path fir IIIF viewer.
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
const indexHtml = join(DIST_FOLDER, 'index.html');
const cookieParser = require('cookie-parser');
@@ -258,7 +257,6 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
baseUrl: environment.ui.nameSpace,
originUrl: environment.ui.baseUrl,
requestUrl: req.originalUrl,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
}, (err, data) => {
if (hasNoValue(err) && hasValue(data)) {
// save server side rendered page to cache (if any are enabled)
@@ -292,13 +290,7 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
* @param res current response
*/
function clientSideRender(req, res) {
res.render(indexHtml, {
req,
providers: [{
provide: APP_BASE_HREF,
useValue: req.baseUrl
}]
});
res.sendFile(indexHtml);
}

View File

@@ -27,7 +27,10 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
SharedModule,
RouterModule,
AccessControlRoutingModule,
FormModule
FormModule,
],
exports: [
MembersListComponent,
],
declarations: [
EPeopleRegistryComponent,
@@ -35,7 +38,7 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
GroupsRegistryComponent,
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent
MembersListComponent,
],
providers: [
{

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: epersonDto.eperson.name} }}">
<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: epersonDto.eperson.name} }}">
<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';
@Component({
selector: 'ds-eperson-form',
@@ -491,7 +492,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

@@ -65,18 +65,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: ePerson.eperson.name} }}">
<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: ePerson.eperson.name} }}">
<i class="fas fa-plus fa-fw"></i>
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
@@ -123,10 +125,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: ePerson.eperson.name} }}">
<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: ePerson.eperson.name} }}">
<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';
@@ -37,10 +37,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(() => {
@@ -53,7 +53,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>>> {
@@ -147,8 +147,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) => {
@@ -167,12 +169,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', () => {
@@ -181,62 +190,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

@@ -11,7 +11,7 @@ import {
ObservedValueOf,
} from 'rxjs';
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
@@ -19,11 +19,13 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model';
import {
getFirstSucceededRemoteData,
getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload
getFirstCompletedRemoteData,
getAllCompletedRemoteData,
getRemoteDataPayload
} from '../../../../core/shared/operators';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model';
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
/**
@@ -35,6 +37,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'
@@ -47,6 +78,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
*/
@@ -91,21 +136,20 @@ 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) {
constructor(
protected groupDataService: GroupDataService,
public ePersonDataService: EPersonDataService,
protected translateService: TranslateService,
protected notificationsService: NotificationsService,
protected formBuilder: FormBuilder,
protected paginationService: PaginationService,
private router: Router
) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
}
ngOnInit() {
ngOnInit(): void {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
@@ -124,7 +168,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(
@@ -135,36 +179,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> {
@@ -193,7 +237,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);
@@ -205,6 +249,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);
@@ -267,7 +312,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

@@ -65,7 +65,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';
@@ -46,8 +38,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;
@@ -65,7 +57,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));
@@ -133,6 +125,7 @@ describe('SubgroupsListComponent', () => {
});
afterEach(fakeAsync(() => {
fixture.destroy();
fixture.debugElement.nativeElement.remove();
flush();
component = null;
}));
@@ -152,7 +145,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', {
@@ -170,7 +163,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'));
@@ -181,9 +174,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();
});
@@ -195,30 +188,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

@@ -29,7 +29,7 @@
<tbody>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
<td>
<label>
<label class="mb-0">
<input type="checkbox"
[checked]="isSelected(bitstreamFormat) | async"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"

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

@@ -34,14 +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)">{{field.id}}</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)">{{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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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.selectedGroup.name }));
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">
{{supervisionOrder.group.name}}
<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,82 @@
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';
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>();
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,158 @@
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } 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';
@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> {
/**
* 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(
private componentFactoryResolver: ComponentFactoryResolver,
private linkService: LinkService,
protected truncatableService: TruncatableService,
private themeService: ThemeService,
protected bitstreamDataService: BitstreamDataService,
protected supervisionOrderDataService: SupervisionOrderDataService,
) {
super(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,
protected 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

@@ -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

@@ -9,6 +9,8 @@ import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
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';
@@ -101,6 +103,8 @@ const PROVIDERS = [
useClass: LogInterceptor,
multi: true
},
// register the dynamic matcher used by form. MUST be provided by the app module
...DYNAMIC_MATCHER_PROVIDERS,
];
const DECLARATIONS = [

View File

@@ -1,7 +1,9 @@
<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]="parentContext.name">
</ds-comcol-page-header>
@@ -22,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

@@ -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

@@ -10,6 +10,7 @@ import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/t
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
@@ -28,6 +29,7 @@ const ENTRY_COMPONENTS = [
SharedBrowseByModule,
CommonModule,
ComcolModule,
DsoPageModule
],
declarations: [
BrowseBySwitcherComponent,

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: [
@@ -91,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

@@ -17,6 +17,7 @@ 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: [
@@ -28,6 +29,7 @@ import { DsoSharedModule } from '../dso-shared/dso-shared.module';
CollectionFormModule,
ComcolModule,
DsoSharedModule,
DsoPageModule,
],
declarations: [
CollectionPageComponent,

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

@@ -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: [
@@ -73,7 +75,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
DSOBreadcrumbsService,
LinkService,
CreateCommunityPageGuard,
CommunityPageAdministratorGuard
CommunityPageAdministratorGuard,
]
})
export class CommunityPageRoutingModule {

View File

@@ -20,8 +20,9 @@
[title]="'community.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="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
</div>
</div>
<section class="comcol-page-browse-section">

View File

@@ -19,6 +19,7 @@ import {
import {
ThemedCollectionPageSubCollectionListComponent
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
const DECLARATIONS = [CommunityPageComponent,
ThemedCommunityPageComponent,
@@ -37,6 +38,7 @@ const DECLARATIONS = [CommunityPageComponent,
StatisticsModule.forRoot(),
CommunityFormModule,
ComcolModule,
DsoPageModule,
],
declarations: [
...DECLARATIONS

View File

@@ -39,7 +39,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsProviderService<ChildHA
return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe(
find((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => parentRD.hasSucceeded || parentRD.statusCode === 204),
switchMap((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => {
if (hasValue(parentRD.payload)) {
if (hasValue(parentRD) && hasValue(parentRD.payload)) {
const parent = parentRD.payload;
return this.getBreadcrumbs(parent, getDSORoute(parent));
}

View File

@@ -7,7 +7,6 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
import { RouteEffects } from './services/route.effects';
import { RouterEffects } from './router/router.effects';
import { MenuEffects } from '../shared/menu/menu.effects';
export const coreEffects = [
RequestEffects,
@@ -19,5 +18,4 @@ export const coreEffects = [
ObjectUpdatesEffects,
RouteEffects,
RouterEffects,
MenuEffects
];

View File

@@ -157,6 +157,9 @@ import { SequenceService } from './shared/sequence.service';
import { CoreState } from './core-state.model';
import { GroupDataService } from './eperson/group-data.service';
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model';
import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model';
import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model';
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
import { AccessStatusDataService } from './data/access-status-data.service';
import { LinkHeadService } from './services/link-head.service';
@@ -170,6 +173,9 @@ import { OrcidHistory } from './orcid/model/orcid-history.model';
import { OrcidAuthService } from './orcid/orcid-auth.service';
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
import { Subscription } from '../shared/subscriptions/models/subscription.model';
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -292,6 +298,7 @@ const PROVIDERS = [
OrcidAuthService,
OrcidQueueDataService,
OrcidHistoryDataService,
SupervisionOrderDataService
];
/**
@@ -338,6 +345,9 @@ export const models =
Version,
VersionHistory,
WorkflowAction,
AdvancedWorkflowInfo,
RatingAdvancedWorkflowInfo,
SelectReviewerAdvancedWorkflowInfo,
TemplateItem,
Feature,
Authorization,
@@ -356,7 +366,9 @@ export const models =
ResearcherProfile,
OrcidQueue,
OrcidHistory,
AccessStatusObject
AccessStatusObject,
IdentifierData,
Subscription,
];
@NgModule({

View File

@@ -32,4 +32,6 @@ export enum FeatureID {
CanSynchronizeWithORCID = 'canSynchronizeWithORCID',
CanSubmit = 'canSubmit',
CanEditItem = 'canEditItem',
CanRegisterDOI = 'canRegisterDOI',
CanSubscribe = 'canSubscribeDso',
}

View File

@@ -0,0 +1,85 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from './base/data-service.decorator';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { BaseDataService } from './base/base-data.service';
import { RequestService } from './request.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { CoreState } from '../core-state.model';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { Item } from '../shared/item.model';
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { map, switchMap } from 'rxjs/operators';
import {ConfigurationProperty} from '../shared/configuration-property.model';
import {ConfigurationDataService} from './configuration-data.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from './request.models';
import { sendRequest } from '../shared/request.operators';
import { RestRequest } from './rest-request.model';
/**
* The service handling all REST requests to get item identifiers like handles and DOIs
* from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI'
* button appears for admins in the item status page
*/
@Injectable()
@dataService(IDENTIFIERS)
export class IdentifierDataService extends BaseDataService<IdentifierData> {
constructor(
protected comparator: DefaultChangeAnalyzer<IdentifierData>,
protected halService: HALEndpointService,
protected http: HttpClient,
protected notificationsService: NotificationsService,
protected objectCache: ObjectCacheService,
protected rdbService: RemoteDataBuildService,
protected requestService: RequestService,
protected store: Store<CoreState>,
private configurationService: ConfigurationDataService,
) {
super('identifiers', requestService, rdbService, objectCache, halService);
}
/**
* Returns {@link RemoteData} of {@link IdentifierData} representing identifiers for this item
* @param item Item we are querying
*/
getIdentifierDataFor(item: Item): Observable<RemoteData<IdentifierData>> {
return this.findByHref(item._links.identifiers.href, false, true);
}
/**
* Should we allow registration of new DOIs via the item status page?
*/
public getIdentifierRegistrationConfiguration(): Observable<string[]> {
return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstCompletedRemoteData(),
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
);
}
public registerIdentifier(item: Item, type: string): Observable<RemoteData<any>> {
const requestId = this.requestService.generateRequestId();
return this.getEndpoint().pipe(
map((endpointURL: string) => {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
let params = new HttpParams();
params = params.append('type', type);
options.params = params;
return new PostRequest(requestId, endpointURL, item._links.self.href, options);
}),
sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<any>>)
);
}
}

View File

@@ -232,6 +232,16 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
return this.rdbService.buildFromRequestUUID(requestId);
}
/**
* Get the endpoint for an item's identifiers
* @param itemId
*/
public getIdentifiersEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`))
);
}
/**
* Get the endpoint to move the item
* @param itemId

View File

@@ -594,6 +594,19 @@ describe('RequestService', () => {
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123'
);
});
it('should properly encode the body with an array', () => {
const body = {
'property1': 'multiple\nlines\nto\nsend',
'property2': 'sp&ci@l characters',
'sp&ci@l-chars in prop': 'test123',
'arrayParam': ['arrayValue1', 'arrayValue2'],
};
const queryParams = service.uriEncodeBody(body);
expect(queryParams).toEqual(
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123&arrayParam=arrayValue1&arrayParam=arrayValue2'
);
});
});
describe('setStaleByUUID', () => {

View File

@@ -255,8 +255,8 @@ export class RequestService {
/**
* Convert request Payload to a URL-encoded string
*
* e.g. uriEncodeBody({param: value, param1: value1})
* returns: param=value&param1=value1
* e.g. uriEncodeBody({param: value, param1: value1, param2: [value3, value4]})
* returns: param=value&param1=value1&param2=value3&param2=value4
*
* @param body
* The request Payload to convert
@@ -267,11 +267,19 @@ export class RequestService {
let queryParams = '';
if (isNotEmpty(body) && typeof body === 'object') {
Object.keys(body)
.forEach((param) => {
.forEach((param: string) => {
const encodedParam = encodeURIComponent(param);
const encodedBody = encodeURIComponent(body[param]);
const paramValue = `${encodedParam}=${encodedBody}`;
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
if (Array.isArray(body[param])) {
for (const element of body[param]) {
const encodedBody = encodeURIComponent(element);
const paramValue = `${encodedParam}=${encodedBody}`;
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
}
} else {
const encodedBody = encodeURIComponent(body[param]);
const paramValue = `${encodedParam}=${encodedBody}`;
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
}
});
}
return queryParams;

View File

@@ -0,0 +1,13 @@
import { SystemWideAlertDataService } from './system-wide-alert-data.service';
import { testFindAllDataImplementation } from './base/find-all-data.spec';
import { testPutDataImplementation } from './base/put-data.spec';
import { testCreateDataImplementation } from './base/create-data.spec';
describe('SystemWideAlertDataService', () => {
describe('composition', () => {
const initService = () => new SystemWideAlertDataService(null, null, null, null, null);
testFindAllDataImplementation(initService);
testPutDataImplementation(initService);
testCreateDataImplementation(initService);
});
});

View File

@@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Observable } from 'rxjs';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
import { FindListOptions } from './find-list-options.model';
import { dataService } from './base/data-service.decorator';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CreateData, CreateDataImpl } from './base/create-data';
import { SYSTEMWIDEALERT } from '../../system-wide-alert/system-wide-alert.resource-type';
import { SystemWideAlert } from '../../system-wide-alert/system-wide-alert.model';
import { PutData, PutDataImpl } from './base/put-data';
import { RequestParam } from '../cache/models/request-param.model';
import { SearchData, SearchDataImpl } from './base/search-data';
/**
* Dataservice representing a system-wide alert
*/
@Injectable()
@dataService(SYSTEMWIDEALERT)
export class SystemWideAlertDataService extends IdentifiableDataService<SystemWideAlert> implements FindAllData<SystemWideAlert>, CreateData<SystemWideAlert>, PutData<SystemWideAlert>, SearchData<SystemWideAlert> {
private findAllData: FindAllDataImpl<SystemWideAlert>;
private createData: CreateDataImpl<SystemWideAlert>;
private putData: PutDataImpl<SystemWideAlert>;
private searchData: SearchData<SystemWideAlert>;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
) {
super('systemwidealerts', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.putData = new PutDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
* info should be added to the objects
*
* @param options Find list options object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>>}
* Return an observable that emits object list
*/
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SystemWideAlert>[]): Observable<RemoteData<PaginatedList<SystemWideAlert>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Create a new object on the server, and store the response in the object cache
*
* @param object The object to create
* @param params Array with additional params to combine with query string
*/
create(object: SystemWideAlert, ...params: RequestParam[]): Observable<RemoteData<SystemWideAlert>> {
return this.createData.create(object, ...params);
}
/**
* Send a PUT request for the specified object
*
* @param object The object to send a put request for.
*/
put(object: SystemWideAlert): Observable<RemoteData<SystemWideAlert>> {
return this.putData.put(object);
}
/**
* Make a new FindListRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SystemWideAlert>[]): Observable<RemoteData<PaginatedList<SystemWideAlert>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -5,15 +5,15 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Injectable } from '@angular/core';
import { WORKFLOW_ACTION } from '../tasks/models/workflow-action-object.resource-type';
import { BaseDataService } from './base/base-data.service';
import { dataService } from './base/data-service.decorator';
import { IdentifiableDataService } from './base/identifiable-data.service';
/**
* A service responsible for fetching/sending data from/to the REST API on the workflowactions endpoint
*/
@Injectable()
@dataService(WORKFLOW_ACTION)
export class WorkflowActionDataService extends BaseDataService<WorkflowAction> {
export class WorkflowActionDataService extends IdentifiableDataService<WorkflowAction> {
protected linkPath = 'workflowactions';
constructor(

View File

@@ -52,7 +52,7 @@ describe(`LocaleInterceptor`, () => {
expect(httpRequest.request.headers.has('Accept-Language'));
const lang = httpRequest.request.headers.get('Accept-Language');
expect(lang).toBeDefined();
expect(lang).not.toBeNull();
expect(lang).toBe(languageList.toString());
});

View File

@@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Operation, ReplaceOperation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import { find, map } from 'rxjs/operators';
import { Observable, of as observableOf } from 'rxjs';
import { find, map, mergeMap } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -130,6 +130,24 @@ export class ResearcherProfileDataService extends IdentifiableDataService<Resear
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
}
/**
* Creates a researcher profile starting from an external source URI and returns the related item's ID
* Emits null if the researcher profile doesn't exist after sending out the request
* @param sourceUri
*/
createFromExternalSourceAndReturnRelatedItemId(sourceUri: string): Observable<string> {
return this.createFromExternalSource(sourceUri).pipe(
getFirstCompletedRemoteData(),
mergeMap((rd: RemoteData<ResearcherProfile>) => {
if (rd.hasSucceeded) {
return this.findRelatedItemId(rd.payload);
} else {
return observableOf(null);
}
}),
);
}
/**
* Create a new object on the server, and store the response in the object cache

View File

@@ -1,16 +1,58 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { XhrFactory } from '@angular/common';
import { Injectable } from '@angular/core';
import { Agent as HttpAgent, AgentOptions as HttpAgentOptions } from 'http';
import { Agent as HttpsAgent } from 'https';
import { prototype, XMLHttpRequest } from 'xhr2';
/**
* Overrides the default XhrFactory server side, to allow us to set cookies in requests to the
* backend. This was added to be able to perform a working XSRF request from the node server, as it
* needs to set a cookie for the XSRF token
* Allow HTTP sessions to be kept alive.
* Without this configuration, Angular re-connects to REST multiple times per SSR cycle.
* https://nodejs.org/api/http.html#new-agentoptions
*/
const agentOptions: HttpAgentOptions = {
keepAlive: true,
keepAliveMsecs: 60 * 1000,
};
// Agents need to be reused between requests, otherwise keep-alive doesn't help.
const httpAgent = new HttpAgent(agentOptions);
const httpsAgent = new HttpsAgent(agentOptions);
/**
* Contructs the XMLHttpRequest instances used for all HttpClient requests.
* Emulated by https://github.com/pwnall/node-xhr2 on the server.
* This class overrides the built-in Angular implementation to set additional configuration.
*
* Changes:
* - Turn off restriction for cookie headers to allow us to set cookies in requests to the backend.
* This was added to be able to perform a working XSRF request from the node server, as it needs to set a cookie for the XSRF token.
* - Override NodeJS HTTP(S) agents to keep sessions alive between requests.
* This improves SSR performance by reducing REST request overhead on the server.
*
* Note that this must be provided in ServerAppModule;
* it doesn't work when added as a Universal engine provider.
*/
@Injectable()
export class ServerXhrService implements XhrFactory {
build(): XMLHttpRequest {
prototype._restrictedHeaders.cookie = false;
return new XMLHttpRequest();
const xhr = new XMLHttpRequest();
// This call is specific to xhr2 and will probably break if we use another library.
// https://github.com/pwnall/node-xhr2#features
(xhr as any).nodejsSet({
httpAgent,
httpsAgent,
});
return xhr;
}
}

View File

@@ -8,6 +8,7 @@ export enum Context {
Search = 'search',
Workflow = 'workflow',
Workspace = 'workspace',
SupervisedItems = 'supervisedWorkspace',
AdminMenu = 'adminMenu',
EntitySearchModalWithNameVariants = 'EntitySearchModalWithNameVariants',
EntitySearchModal = 'EntitySearchModal',

View File

@@ -24,6 +24,8 @@ import { Bitstream } from './bitstream.model';
import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
import { HandleObject } from './handle-object.model';
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
/**
* Class representing a DSpace Item
@@ -76,6 +78,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
version: HALLink;
thumbnail: HALLink;
accessStatus: HALLink;
identifiers: HALLink;
self: HALLink;
};
@@ -121,6 +124,13 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
@link(ACCESS_STATUS)
accessStatus?: Observable<RemoteData<AccessStatusObject>>;
/**
* The identifier data for this Item
* Will be undefined unless the identifiers {@link HALLink} has been resolved.
*/
@link(IDENTIFIERS, false, 'identifiers')
identifiers?: Observable<RemoteData<IdentifierData>>;
/**
* Method that returns as which type of object this object should be rendered
*/

View File

@@ -14,6 +14,9 @@ import { ITEM } from '../../shared/item.resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { SUPERVISION_ORDER } from '../../supervision-order/models/supervision-order.resource-type';
import { PaginatedList } from '../../data/paginated-list.model';
import { SupervisionOrder } from '../../supervision-order/models/supervision-order.model';
export interface SubmissionObjectError {
message: string;
@@ -65,6 +68,7 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable
item: HALLink;
submissionDefinition: HALLink;
submitter: HALLink;
supervisionOrders: HALLink;
};
get self(): string {
@@ -93,4 +97,12 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable
@link(EPERSON)
submitter?: Observable<RemoteData<EPerson>> | EPerson;
/**
* The submission supervision order
* Will be undefined unless the workspace item {@link HALLink} has been resolved.
*/
@link(SUPERVISION_ORDER)
/* This was changed from 'Observable<RemoteData<WorkspaceItem>> | WorkspaceItem' to 'any' to prevent issues in templates with async */
supervisionOrders?: Observable<RemoteData<PaginatedList<SupervisionOrder>>>;
}

View File

@@ -0,0 +1,9 @@
/*
* Object model for the data returned by the REST API to present minted identifiers in a submission section
*/
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
export interface WorkspaceitemSectionIdentifiersObject {
identifiers?: Identifier[]
displayTypes?: string[]
}

View File

@@ -3,6 +3,7 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model';
import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model';
import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
/**
@@ -23,4 +24,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
| WorkspaceitemSectionSherpaPoliciesObject
| WorkspaceitemSectionIdentifiersObject
| string;

View File

@@ -0,0 +1,44 @@
/**
* Enum representing the Action Type of a Resource Policy
*/
export enum ActionType {
/**
* Action of reading, viewing or downloading something
*/
READ = 'READ',
/**
* Action of modifying something
*/
WRITE = 'WRITE',
/**
* Action of deleting something
*/
DELETE = 'DELETE',
/**
* Action of adding something to a container
*/
ADD = 'ADD',
/**
* Action of removing something from a container
*/
REMOVE = 'REMOVE',
/**
* None Type of Supervision Order
*/
NONE = 'NONE',
/**
* Editor Type of Supervision Order
*/
EDITOR = 'EDITOR',
/**
* Observer Type of Supervision Order
*/
OBSERVER = 'OBSERVER',
}

View File

@@ -0,0 +1,74 @@
import { autoserialize, deserialize, deserializeAs } from 'cerialize';
import { link, typedObject } from '../../cache/builders/build-decorators';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { HALLink } from '../../shared/hal-link.model';
import { SUPERVISION_ORDER } from './supervision-order.resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { Observable } from 'rxjs';
import { RemoteData } from '../../data/remote-data';
import { GROUP } from '../../eperson/models/group.resource-type';
import { Group } from '../../eperson/models/group.model';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { ITEM } from '../../shared/item.resource-type';
import { Item } from '../../shared/item.model';
/**
* Model class for a Supervision Order
*/
@typedObject
export class SupervisionOrder implements CacheableObject {
static type = SUPERVISION_ORDER;
/**
* The identifier for this Supervision Order
*/
@autoserialize
id: string;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
ordertype: string;
/**
* The universally unique identifier for this Supervision Order
* This UUID is generated client-side and isn't used by the backend.
* It is based on the ID, so it will be the same for each refresh.
*/
@deserializeAs(new IDToUUIDSerializer('supervision-order'), 'id')
uuid: string;
/**
* The {@link HALLink}s for this SupervisionOrder
*/
@deserialize
_links: {
item: HALLink,
group: HALLink,
self: HALLink,
};
/**
* The related supervision Item
* Will be undefined unless the item {@link HALLink} has been resolved.
*/
@link(ITEM)
item?: Observable<RemoteData<Item>>;
/**
* The group linked by this supervision order
* Will be undefined unless the version {@link HALLink} has been resolved.
*/
@link(GROUP)
group?: Observable<RemoteData<Group>>;
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for SupervisionOrder
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SUPERVISION_ORDER = new ResourceType('supervisionorder');

View File

@@ -0,0 +1,277 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { SupervisionOrderDataService } from './supervision-order-data.service';
import { ActionType } from './models/action-type.model';
import { RequestParam } from '../cache/models/request-param.model';
import { PageInfo } from '../shared/page-info.model';
import { buildPaginatedList } from '../data/paginated-list.model';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { RestResponse } from '../cache/response.models';
import { RequestEntry } from '../data/request-entry.model';
import { FindListOptions } from '../data/find-list-options.model';
import { GroupDataService } from '../eperson/group-data.service';
describe('SupervisionOrderService', () => {
let scheduler: TestScheduler;
let service: SupervisionOrderDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let responseCacheEntry: RequestEntry;
let groupService: GroupDataService;
const supervisionOrder: any = {
id: '1',
name: null,
description: null,
action: ActionType.READ,
startDate: null,
endDate: null,
type: 'supervisionOrder',
uuid: 'supervision-order-1',
_links: {
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/supervisionorder/1'
},
}
};
const anothersupervisionOrder: any = {
id: '2',
name: null,
description: null,
action: ActionType.WRITE,
startDate: null,
endDate: null,
type: 'supervisionOrder',
uuid: 'supervision-order-2',
_links: {
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/supervisionorder/1'
},
}
};
const endpointURL = `https://rest.api/rest/api/supervisionorder`;
const requestURL = `https://rest.api/rest/api/supervisionorder/${supervisionOrder.id}`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const supervisionOrderId = '1';
const groupUUID = '8b39g7ya-5a4b-438b-9686-be1d5b4a1c5a';
const itemUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a';
const supervisionOrderType = 'NONE';
const pageInfo = new PageInfo();
const array = [supervisionOrder, anothersupervisionOrder];
const paginatedList = buildPaginatedList(pageInfo, array);
const supervisionOrderRD = createSuccessfulRemoteDataObject(supervisionOrder);
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
const groupEndpoint = 'group_EP';
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
setStaleByHrefSubstring: {},
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('a|', {
a: supervisionOrderRD
}),
buildList: hot('a|', {
a: paginatedListRD
}),
buildFromRequestUUID: hot('a|', {
a: supervisionOrderRD
}),
buildFromRequestUUIDAndAwait: hot('a|', {
a: supervisionOrderRD
})
});
groupService = jasmine.createSpyObj('groupService', {
getBrowseEndpoint: hot('a', {
a: groupEndpoint
}),
getIDHrefObs: cold('a', {
a: 'https://rest.api/rest/api/group/groups/' + groupUUID
}),
});
groupService = jasmine.createSpyObj('groupService', {
getIDHrefObs: cold('a', {
a: 'https://rest.api/rest/api/group/groups/' + groupUUID
}),
});
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const comparator = {} as any;
service = new SupervisionOrderDataService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
comparator,
groupService,
);
spyOn(service, 'findById').and.callThrough();
spyOn(service, 'findByHref').and.callThrough();
spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
spyOn((service as any).createData, 'create').and.callThrough();
spyOn((service as any).deleteData, 'delete').and.callThrough();
spyOn((service as any).patchData, 'update').and.callThrough();
spyOn((service as any).searchData, 'searchBy').and.callThrough();
spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(observableOf(requestURL));
});
describe('create', () => {
it('should proxy the call to createData.create with group UUID', () => {
scheduler.schedule(() => service.create(supervisionOrder, itemUUID, groupUUID, supervisionOrderType));
const params = [
new RequestParam('uuid', itemUUID),
new RequestParam('group', groupUUID),
new RequestParam('type', supervisionOrderType),
];
scheduler.flush();
expect((service as any).createData.create).toHaveBeenCalledWith(supervisionOrder, ...params);
});
it('should proxy the call to createData.create with group UUID', () => {
scheduler.schedule(() => service.create(supervisionOrder, itemUUID, groupUUID, supervisionOrderType));
const params = [
new RequestParam('uuid', itemUUID),
new RequestParam('group', groupUUID),
new RequestParam('type', supervisionOrderType),
];
scheduler.flush();
expect((service as any).createData.create).toHaveBeenCalledWith(supervisionOrder, ...params);
});
it('should return a RemoteData<supervisionOrder> for the object with the given id', () => {
const result = service.create(supervisionOrder, itemUUID, groupUUID, supervisionOrderType);
const expected = cold('a|', {
a: supervisionOrderRD
});
expect(result).toBeObservable(expected);
});
});
describe('delete', () => {
it('should proxy the call to deleteData.delete', () => {
scheduler.schedule(() => service.delete(supervisionOrderId));
scheduler.flush();
expect((service as any).deleteData.delete).toHaveBeenCalledWith(supervisionOrderId);
});
});
describe('update', () => {
it('should proxy the call to updateData.update', () => {
scheduler.schedule(() => service.update(supervisionOrder));
scheduler.flush();
expect((service as any).patchData.update).toHaveBeenCalledWith(supervisionOrder);
});
});
describe('findById', () => {
it('should return a RemoteData<supervisionOrder> for the object with the given id', () => {
const result = service.findById(supervisionOrderId);
const expected = cold('a|', {
a: supervisionOrderRD
});
expect(result).toBeObservable(expected);
});
});
describe('findByHref', () => {
it('should return a RemoteData<supervisionOrder> for the object with the given URL', () => {
const result = service.findByHref(requestURL);
const expected = cold('a|', {
a: supervisionOrderRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByGroup', () => {
it('should proxy the call to searchData.searchBy', () => {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', groupUUID)];
scheduler.schedule(() => service.searchByGroup(groupUUID));
scheduler.flush();
expect((service as any).searchData.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options, true, true);
});
it('should proxy the call to searchData.searchBy with additional search param', () => {
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', groupUUID),
new RequestParam('item', itemUUID),
];
scheduler.schedule(() => service.searchByGroup(groupUUID, itemUUID));
scheduler.flush();
expect((service as any).searchData.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options, true, true);
});
it('should return a RemoteData<PaginatedList<supervisionOrder>) for the search', () => {
const result = service.searchByGroup(groupUUID);
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByItem', () => {
it('should proxy the call to searchData.searchBy', () => {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', itemUUID)];
scheduler.schedule(() => service.searchByItem(itemUUID));
scheduler.flush();
expect((service as any).searchData.searchBy).toHaveBeenCalledWith((service as any).searchByItemMethod, options, true, true);
});
it('should return a RemoteData<PaginatedList<supervisionOrder>) for the search', () => {
const result = service.searchByItem(itemUUID);
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
});

View File

@@ -0,0 +1,181 @@
import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { SupervisionOrder } from './models/supervision-order.model';
import { RemoteData } from '../data/remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SUPERVISION_ORDER } from './models/supervision-order.resource-type';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list.model';
import { RequestParam } from '../cache/models/request-param.model';
import { isNotEmpty } from '../../shared/empty.util';
import { first, map } from 'rxjs/operators';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { FindListOptions } from '../data/find-list-options.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PutRequest } from '../data/request.models';
import { GenericConstructor } from '../shared/generic-constructor';
import { ResponseParsingService } from '../data/parsing.service';
import { StatusCodeOnlyResponseParsingService } from '../data/status-code-only-response-parsing.service';
import { GroupDataService } from '../eperson/group-data.service';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { CreateDataImpl } from '../data/base/create-data';
import { SearchDataImpl } from '../data/base/search-data';
import { PatchDataImpl } from '../data/base/patch-data';
import { DeleteDataImpl } from '../data/base/delete-data';
import { dataService } from '../data/base/data-service.decorator';
/**
* A service responsible for fetching/sending data from/to the REST API on the supervisionorders endpoint
*/
@Injectable()
@dataService(SUPERVISION_ORDER)
export class SupervisionOrderDataService extends IdentifiableDataService<SupervisionOrder> {
protected searchByGroupMethod = 'group';
protected searchByItemMethod = 'byItem';
private createData: CreateDataImpl<SupervisionOrder>;
private searchData: SearchDataImpl<SupervisionOrder>;
private patchData: PatchDataImpl<SupervisionOrder>;
private deleteData: DeleteDataImpl<SupervisionOrder>;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected comparator: DefaultChangeAnalyzer<SupervisionOrder>,
protected groupService: GroupDataService,
) {
super('supervisionorders', requestService, rdbService, objectCache, halService);
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
}
/**
* Create a new SupervisionOrder on the server, and store the response
* in the object cache
*
* @param {SupervisionOrder} supervisionOrder
* The supervision order to create
* @param {string} itemUUID
* The uuid of the item that will be grant of the permission.
* @param {string} groupUUID
* The uuid of the group that will be grant of the permission.
* @param {string} type
* The type of the supervision order that will be grant of the permission.
*/
create(supervisionOrder: SupervisionOrder, itemUUID: string, groupUUID: string, type: string): Observable<RemoteData<SupervisionOrder>> {
const params = [];
params.push(new RequestParam('uuid', itemUUID));
params.push(new RequestParam('group', groupUUID));
params.push(new RequestParam('type', type));
return this.createData.create(supervisionOrder, ...params);
}
/**
* Delete an existing SupervisionOrder on the server
*
* @param supervisionOrderID The supervision order's id to be removed
* @return an observable that emits true when the deletion was successful, false when it failed
*/
delete(supervisionOrderID: string): Observable<boolean> {
return this.deleteData.delete(supervisionOrderID).pipe(
getFirstCompletedRemoteData(),
map((response: RemoteData<NoContent>) => response.hasSucceeded),
);
}
/**
* Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache
* @param {SupervisionOrder} object The given object
*/
update(object: SupervisionOrder): Observable<RemoteData<SupervisionOrder>> {
return this.patchData.update(object);
}
/**
* Return the {@link SupervisionOrder} list for a {@link Group}
*
* @param UUID UUID of a given {@link Group}
* @param itemUUID Limit the returned policies to the specified DSO
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
searchByGroup(UUID: string, itemUUID?: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SupervisionOrder>[]): Observable<RemoteData<PaginatedList<SupervisionOrder>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', UUID)];
if (isNotEmpty(itemUUID)) {
options.searchParams.push(new RequestParam('item', itemUUID));
}
return this.searchData.searchBy(this.searchByGroupMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Return the {@link SupervisionOrder} list for a given DSO
*
* @param UUID UUID of a given DSO
* @param action Limit the returned policies to the specified {@link ActionType}
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
searchByItem(UUID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SupervisionOrder>[]): Observable<RemoteData<PaginatedList<SupervisionOrder>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', UUID)];
return this.searchData.searchBy(this.searchByItemMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Update the target of the supervision order
* @param supervisionOrderId the ID of the supervision order
* @param supervisionOrderHref the link to the supervision order
* @param targetUUID the UUID of the target to which the permission is being granted
* @param targetType the type of the target (eperson or group) to which the permission is being granted
*/
updateTarget(supervisionOrderId: string, supervisionOrderHref: string, targetUUID: string, targetType: string): Observable<RemoteData<any>> {
const targetService = this.groupService;
const targetEndpoint$ = targetService.getIDHrefObs(targetUUID);
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
const requestId = this.requestService.generateRequestId();
targetEndpoint$.pipe(
first(),
).subscribe((targetEndpoint) => {
const resourceEndpoint = supervisionOrderHref + '/' + targetType;
const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options);
Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return StatusCodeOnlyResponseParsingService;
}
});
this.requestService.send(request);
});
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(supervisionOrderHref));
}
}

View File

@@ -0,0 +1,11 @@
import { autoserialize } from 'cerialize';
/**
* An abstract model class for a {@link AdvancedWorkflowInfo}
*/
export abstract class AdvancedWorkflowInfo {
@autoserialize
id: string;
}

View File

@@ -0,0 +1,17 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for {@link RatingAdvancedWorkflowInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const RATING_ADVANCED_WORKFLOW_INFO = new ResourceType('ratingrevieweraction');
/**
* The resource type for {@link SelectReviewerAdvancedWorkflowInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO = new ResourceType('selectrevieweraction');

View File

@@ -0,0 +1,28 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { RATING_ADVANCED_WORKFLOW_INFO } from './advanced-workflow-info.resource-type';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
import { ResourceType } from '../../shared/resource-type';
/**
* A model class for a {@link RatingAdvancedWorkflowInfo}
*/
@typedObject
@inheritSerialization(AdvancedWorkflowInfo)
export class RatingAdvancedWorkflowInfo extends AdvancedWorkflowInfo {
static type: ResourceType = RATING_ADVANCED_WORKFLOW_INFO;
/**
* Whether the description is required.
*/
@autoserialize
descriptionRequired: boolean;
/**
* The maximum value.
*/
@autoserialize
maxValue: number;
}

View File

@@ -0,0 +1,19 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO } from './advanced-workflow-info.resource-type';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
import { ResourceType } from '../../shared/resource-type';
/**
* A model class for a {@link SelectReviewerAdvancedWorkflowInfo}
*/
@typedObject
@inheritSerialization(AdvancedWorkflowInfo)
export class SelectReviewerAdvancedWorkflowInfo extends AdvancedWorkflowInfo {
static type: ResourceType = SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO;
@autoserialize
group: string;
}

View File

@@ -2,6 +2,7 @@ import { inheritSerialization, autoserialize } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { WORKFLOW_ACTION } from './workflow-action-object.resource-type';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
/**
* A model class for a WorkflowAction
@@ -22,4 +23,23 @@ export class WorkflowAction extends DSpaceObject {
*/
@autoserialize
options: string[];
/**
* Whether this action has advanced options
*/
@autoserialize
advanced: boolean;
/**
* The advanced options that the user can select at this action
*/
@autoserialize
advancedOptions: string[];
/**
* The advanced info required by the advanced options
*/
@autoserialize
advancedInfo: AdvancedWorkflowInfo[];
}

View File

@@ -67,7 +67,7 @@ describe(`XsrfInterceptor`, () => {
expect(httpRequest.request.headers.has('X-XSRF-TOKEN')).toBeTrue();
expect(httpRequest.request.withCredentials).toBeTrue();
const token = httpRequest.request.headers.get('X-XSRF-TOKEN');
expect(token).toBeDefined();
expect(token).not.toBeNull();
expect(token).toBe(testToken.toString());
httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
@@ -116,11 +116,11 @@ describe(`XsrfInterceptor`, () => {
// ensure mock XSRF token is in response
expect(response.headers.has('DSPACE-XSRF-TOKEN')).toBeTrue();
const token = response.headers.get('DSPACE-XSRF-TOKEN');
expect(token).toBeDefined();
expect(token).not.toBeNull();
expect(token).toBe(mockNewXSRFToken.toString());
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
expect(cookieService.get('XSRF-TOKEN')).toBeDefined();
expect(cookieService.get('XSRF-TOKEN')).not.toBeNull();
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
done();
@@ -153,7 +153,7 @@ describe(`XsrfInterceptor`, () => {
expect(error.statusText).toBe(mockErrorText);
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
expect(cookieService.get('XSRF-TOKEN')).toBeDefined();
expect(cookieService.get('XSRF-TOKEN')).not.toBeNull();
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
done();

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalissue.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('JournalIssue', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Journal Issue
*/
export class JournalIssueComponent extends VersionedItemComponent {
export class JournalIssueComponent extends ItemComponent {
}

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalvolume.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

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