diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts
index afb92a9111..47a971a882 100644
--- a/src/app/access-control/access-control.module.ts
+++ b/src/app/access-control/access-control.module.ts
@@ -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: [
{
diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html
index 8b0ae35bd4..282ee89674 100644
--- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html
+++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html
@@ -65,18 +65,20 @@
-
-
|
@@ -123,10 +125,19 @@
-
+
|
diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts
index 8d0ddf0a85..b7536177cd 100644
--- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts
+++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts
@@ -149,6 +149,7 @@ describe('MembersListComponent', () => {
fixture.destroy();
flush();
component = null;
+ fixture.debugElement.nativeElement.remove();
}));
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
index 169d009d63..58d252f0b4 100644
--- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
+++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
@@ -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) => {
- if (rd.hasFailed) {
- this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
- } else {
- return rd;
- }
- }),
- switchMap((epersonListRD: RemoteData>) => {
- const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
- const dto$: Observable = observableCombineLatest(
- this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => {
- 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) => {
+ if (rd.hasFailed) {
+ this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
+ } else {
+ return rd;
+ }
+ }),
+ switchMap((epersonListRD: RemoteData>) => {
+ const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
+ const dto$: Observable = observableCombineLatest(
+ this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => {
+ 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) => {
+ this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
}));
- }))
- .subscribe((paginatedListOfDTOs: PaginatedList) => {
- 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 {
@@ -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);
@@ -267,7 +311,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
getAllCompletedRemoteData(),
map((rd: RemoteData) => {
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;
}
diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
index a6c37cbc45..0177cc3a38 100644
--- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
+++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
@@ -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,
+ ];
+ }),
);
}
}
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 0aed8c0578..1a5c1df2dc 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -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';
@@ -341,6 +344,9 @@ export const models =
Version,
VersionHistory,
WorkflowAction,
+ AdvancedWorkflowInfo,
+ RatingAdvancedWorkflowInfo,
+ SelectReviewerAdvancedWorkflowInfo,
TemplateItem,
Feature,
Authorization,
diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts
index fe35d840d7..108a588881 100644
--- a/src/app/core/data/request.service.spec.ts
+++ b/src/app/core/data/request.service.spec.ts
@@ -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', () => {
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index 16dc14dac4..94a6020975 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -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¶m1=value1
+ * e.g. uriEncodeBody({param: value, param1: value1, param2: [value3, value4]})
+ * returns: param=value¶m1=value1¶m2=value3¶m2=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;
diff --git a/src/app/core/data/workflow-action-data.service.ts b/src/app/core/data/workflow-action-data.service.ts
index b2c4f0bd7b..00cd5e2889 100644
--- a/src/app/core/data/workflow-action-data.service.ts
+++ b/src/app/core/data/workflow-action-data.service.ts
@@ -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 {
+export class WorkflowActionDataService extends IdentifiableDataService {
protected linkPath = 'workflowactions';
constructor(
diff --git a/src/app/core/tasks/models/advanced-workflow-info.model.ts b/src/app/core/tasks/models/advanced-workflow-info.model.ts
new file mode 100644
index 0000000000..87991a375c
--- /dev/null
+++ b/src/app/core/tasks/models/advanced-workflow-info.model.ts
@@ -0,0 +1,11 @@
+import { autoserialize } from 'cerialize';
+
+/**
+ * An abstract model class for a {@link AdvancedWorkflowInfo}
+ */
+export abstract class AdvancedWorkflowInfo {
+
+ @autoserialize
+ id: string;
+
+}
diff --git a/src/app/core/tasks/models/advanced-workflow-info.resource-type.ts b/src/app/core/tasks/models/advanced-workflow-info.resource-type.ts
new file mode 100644
index 0000000000..4e7793f875
--- /dev/null
+++ b/src/app/core/tasks/models/advanced-workflow-info.resource-type.ts
@@ -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');
diff --git a/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts b/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts
new file mode 100644
index 0000000000..b7861d4fe4
--- /dev/null
+++ b/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts
@@ -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;
+
+}
diff --git a/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts b/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts
new file mode 100644
index 0000000000..b87770596e
--- /dev/null
+++ b/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts
@@ -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;
+
+}
diff --git a/src/app/core/tasks/models/workflow-action-object.model.ts b/src/app/core/tasks/models/workflow-action-object.model.ts
index 720d817859..0896e6b8f8 100644
--- a/src/app/core/tasks/models/workflow-action-object.model.ts
+++ b/src/app/core/tasks/models/workflow-action-object.model.ts
@@ -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[];
+
}
diff --git a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
index ce6e01df3d..dbef279d8c 100644
--- a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
+++ b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
@@ -1,4 +1,4 @@
-