Merge remote-tracking branch 'remotes/origin/submission' into mydspace

# Conflicts:
#	resources/i18n/en.json
#	src/app/core/data/request.service.spec.ts
#	src/app/core/data/request.service.ts
#	src/app/core/json-patch/selectors.ts
#	src/app/shared/shared.module.ts
This commit is contained in:
Giuseppe Digilio
2019-03-27 16:01:26 +01:00
107 changed files with 3261 additions and 544 deletions

View File

@@ -410,6 +410,7 @@
"f.dateIssued.min": "Start date", "f.dateIssued.min": "Start date",
"f.dateIssued.max": "End date", "f.dateIssued.max": "End date",
"f.subject": "Subject", "f.subject": "Subject",
"f.has_content_in_original_bundle": "Has files",
"f.namedresourcetype": "Status", "f.namedresourcetype": "Status",
"f.dateSubmitted": "Date submitted", "f.dateSubmitted": "Date submitted",
"f.itemtype": "Type", "f.itemtype": "Type",
@@ -729,7 +730,7 @@
"group-collapse": "Collapse", "group-collapse": "Collapse",
"group-expand": "Expand", "group-expand": "Expand",
"group-collapse-help": "Click here to collapse", "group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more element", "group-expand-help": "Click here to expand and add more elements",
"other-information": { "other-information": {
} }
}, },
@@ -762,6 +763,34 @@
"chips": { "chips": {
"remove": "Remove chip" "remove": "Remove chip"
}, },
"dso-selector": {
"create": {
"community": {
"head": "New community",
"sub-level": "Create a new community in",
"top-level": "Create a new top-level community"
},
"collection": {
"head": "New collection"
},
"item": {
"head": "New item"
}
},
"edit": {
"community": {
"head": "Edit community"
},
"collection": {
"head": "Edit collection"
},
"item": {
"head": "Edit item"
}
},
"placeholder": "Search for a {{ type }}",
"no-results": "No {{ type }} found"
},
"submission": { "submission": {
"general":{ "general":{
"cannot_submit": "You have not the privilege to make a new submission.", "cannot_submit": "You have not the privilege to make a new submission.",
@@ -822,14 +851,16 @@
"upload-successful": "Upload successful", "upload-successful": "Upload successful",
"upload-failed": "Upload failed", "upload-failed": "Upload failed",
"header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", "header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):",
"header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicity decided for the single file, with the following group(s):", "header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):",
"form": { "form": {
"access-condition-label": "Access condition type", "access-condition-label": "Access condition type",
"from-label": "Access grant from", "from-label": "Access grant from",
"from-placeholder": "From", "from-placeholder": "From",
"until-label": "Access grant until", "until-label": "Access grant until",
"until-placeholder": "Until", "until-placeholder": "Until",
"group-label": "Group" "group-label": "Group",
"group-required": "Group is required.",
"date-required": "Date is required."
}, },
"save-metadata": "Save metadata", "save-metadata": "Save metadata",
"undo": "Cancel", "undo": "Cancel",
@@ -894,33 +925,5 @@
"browse": "browse", "browse": "browse",
"queue-lenght": "Queue length", "queue-lenght": "Queue length",
"processing": "Processing" "processing": "Processing"
},
"dso-selector": {
"create": {
"community": {
"head": "New community",
"sub-level": "Create a new community in",
"top-level": "Create a new top-level community"
},
"collection": {
"head": "New collection"
},
"item": {
"head": "New item"
}
},
"edit": {
"community": {
"head": "Edit community"
},
"collection": {
"head": "Edit collection"
},
"item": {
"head": "Edit item"
}
},
"placeholder": "Search for a {{ type }}",
"no-results": "No {{ type }} found"
} }
} }

View File

@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { filter, take } from 'rxjs/operators'; import { filter, take } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -16,17 +16,34 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { isAuthenticated } from '../core/auth/selectors'; import { isAuthenticated } from '../core/auth/selectors';
/**
* This component represents the login page
*/
@Component({ @Component({
selector: 'ds-login-page', selector: 'ds-login-page',
styleUrls: ['./login-page.component.scss'], styleUrls: ['./login-page.component.scss'],
templateUrl: './login-page.component.html' templateUrl: './login-page.component.html'
}) })
export class LoginPageComponent implements OnDestroy, OnInit { export class LoginPageComponent implements OnDestroy, OnInit {
/**
* Subscription to unsubscribe onDestroy
* @type {Subscription}
*/
sub: Subscription; sub: Subscription;
/**
* Initialize instance variables
*
* @param {ActivatedRoute} route
* @param {Store<AppState>} store
*/
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private store: Store<AppState>) {} private store: Store<AppState>) {}
/**
* Initialize instance variables
*/
ngOnInit() { ngOnInit() {
const queryParamsObs = this.route.queryParams; const queryParamsObs = this.route.queryParams;
const authenticated = this.store.select(isAuthenticated); const authenticated = this.store.select(isAuthenticated);
@@ -52,6 +69,9 @@ export class LoginPageComponent implements OnDestroy, OnInit {
}) })
} }
/**
* Unsubscribe from subscription
*/
ngOnDestroy() { ngOnDestroy() {
if (hasValue(this.sub)) { if (hasValue(this.sub)) {
this.sub.unsubscribe(); this.sub.unsubscribe();

View File

@@ -13,6 +13,11 @@ export class AccessConditionOption {
*/ */
groupUUID: string; groupUUID: string;
/**
* The uuid of the Group that contains set of groups this Resource Policy applies to
*/
selectGroupUUID: string;
/** /**
* A boolean representing if this Access Condition has a start date * A boolean representing if this Access Condition has a start date
*/ */

View File

@@ -6,6 +6,9 @@ import { NormalizedSubmissionFormsModel } from './normalized-config-submission-f
import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model'; import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model';
import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model'; import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model';
/**
* Class to return normalized models for config objects
*/
export class ConfigObjectFactory { export class ConfigObjectFactory {
public static getConstructor(type): GenericConstructor<ConfigObject> { public static getConstructor(type): GenericConstructor<ConfigObject> {
switch (type) { switch (type) {

View File

@@ -68,7 +68,7 @@ import { WorkflowitemDataService } from './submission/workflowitem-data.service'
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
import { FileService } from './shared/file.service'; import { FileService } from './shared/file.service';
import { SubmissionRestService } from '../submission/submission-rest.service'; import { SubmissionRestService } from './submission/submission-rest.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';

View File

@@ -53,6 +53,14 @@ export abstract class DataService<T extends CacheableObject> {
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string> public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
/**
* Create the HREF with given options object
*
* @param options The [[FindAllOptions]] object
* @param linkPath The link path for the object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable<string> { protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
let result: Observable<string>; let result: Observable<string>;
const args = []; const args = [];
@@ -62,6 +70,14 @@ export abstract class DataService<T extends CacheableObject> {
return this.buildHrefFromFindOptions(result, args, options); return this.buildHrefFromFindOptions(result, args, options);
} }
/**
* Create the HREF for a specific object's search method with given options object
*
* @param searchMethod The search method for the object
* @param options The [[FindAllOptions]] object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected getSearchByHref(searchMethod: string, options: FindAllOptions = {}): Observable<string> { protected getSearchByHref(searchMethod: string, options: FindAllOptions = {}): Observable<string> {
let result: Observable<string>; let result: Observable<string>;
const args = []; const args = [];
@@ -77,6 +93,15 @@ export abstract class DataService<T extends CacheableObject> {
return this.buildHrefFromFindOptions(result, args, options); return this.buildHrefFromFindOptions(result, args, options);
} }
/**
* Turn an options object into a query string and combine it with the given HREF
*
* @param href$ The HREF to which the query string should be appended
* @param args Array with additional params to combine with query string
* @param options The [[FindAllOptions]] object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected buildHrefFromFindOptions(href$: Observable<string>, args: string[], options: FindAllOptions): Observable<string> { protected buildHrefFromFindOptions(href$: Observable<string>, args: string[], options: FindAllOptions): Observable<string> {
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
@@ -140,12 +165,25 @@ export abstract class DataService<T extends CacheableObject> {
return this.rdbService.buildSingle<T>(href); return this.rdbService.buildSingle<T>(href);
} }
/**
* Return object search endpoint by given search method
*
* @param searchMethod The search method for the object
*/
protected getSearchEndpoint(searchMethod: string): Observable<string> { protected getSearchEndpoint(searchMethod: string): Observable<string> {
return this.halService.getEndpoint(`${this.linkPath}/search`).pipe( return this.halService.getEndpoint(`${this.linkPath}/search`).pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((href: string) => `${href}/${searchMethod}`)); map((href: string) => `${href}/${searchMethod}`));
} }
/**
* Make a new FindAllRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindAllOptions]] object
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
protected searchBy(searchMethod: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<T>>> { protected searchBy(searchMethod: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
const hrefObs = this.getSearchByHref(searchMethod, options); const hrefObs = this.getSearchByHref(searchMethod, options);

View File

@@ -20,7 +20,6 @@ import {
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
describe('RequestService', () => { describe('RequestService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;

View File

@@ -5,8 +5,8 @@ import { Observable, race as observableRace } from 'rxjs';
import { filter, find, mergeMap, take } from 'rxjs/operators'; import { filter, find, mergeMap, take } from 'rxjs/operators';
import { remove } from 'lodash'; import { remove } from 'lodash';
import { AppState } from '../../app.reducer';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { AppState } from '../../app.reducer';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -137,7 +137,6 @@ export class RequestService {
* @param {RestRequest} request The request to send out * @param {RestRequest} request The request to send out
* @param {boolean} forceBypassCache When true, a new request is always dispatched * @param {boolean} forceBypassCache When true, a new request is always dispatched
*/ */
// TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void { configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
const isGetRequest = request.method === RestRequestMethod.GET; const isGetRequest = request.method === RestRequestMethod.GET;
if (forceBypassCache) { if (forceBypassCache) {

View File

@@ -1,22 +1,50 @@
import { Observable } from 'rxjs';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { Group } from './group.model'; import { Group } from './group.model';
import { RemoteData } from '../../data/remote-data';
import { PaginatedList } from '../../data/paginated-list';
export class EPerson extends DSpaceObject { export class EPerson extends DSpaceObject {
/**
* A string representing the unique handle of this Collection
*/
public handle: string; public handle: string;
public groups: Group[]; /**
* List of Groups that this EPerson belong to
*/
public groups: Observable<RemoteData<PaginatedList<Group>>>;
/**
* A string representing the netid of this EPerson
*/
public netid: string; public netid: string;
/**
* A string representing the last active date for this EPerson
*/
public lastActive: string; public lastActive: string;
/**
* A boolean representing if this EPerson can log in
*/
public canLogIn: boolean; public canLogIn: boolean;
/**
* The EPerson email address
*/
public email: string; public email: string;
/**
* A boolean representing if this EPerson require certificate
*/
public requireCertificate: boolean; public requireCertificate: boolean;
/**
* A boolean representing if this EPerson registered itself
*/
public selfRegistered: boolean; public selfRegistered: boolean;
/** Getter to retrieve the EPerson's full name as a string */ /** Getter to retrieve the EPerson's full name as a string */

View File

@@ -1,12 +1,28 @@
import { Observable } from 'rxjs';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
export class Group extends DSpaceObject { export class Group extends DSpaceObject {
public groups: Group[]; /**
* List of Groups that this Group belong to
*/
public groups: Observable<RemoteData<PaginatedList<Group>>>;
/**
* A string representing the unique handle of this Group
*/
public handle: string; public handle: string;
/**
* A string representing the name of this Group
*/
public name: string; public name: string;
/**
* A string representing the name of this Group is permanent
*/
public permanent: boolean; public permanent: boolean;
} }

View File

@@ -1,37 +1,62 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { EPerson } from './eperson.model'; import { EPerson } from './eperson.model';
import { mapsTo } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { Group } from './group.model'; import { ResourceType } from '../../shared/resource-type';
import { NormalizedGroup } from './normalized-group.model';
@mapsTo(EPerson) @mapsTo(EPerson)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject, ListableObject { export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject, ListableObject {
/**
* A string representing the unique handle of this EPerson
*/
@autoserialize @autoserialize
public handle: string; public handle: string;
@autoserializeAs(NormalizedGroup) /**
groups: Group[]; * List of Groups that this EPerson belong to
*/
@deserialize
@relationship(ResourceType.Group, true)
groups: string[];
/**
* A string representing the netid of this EPerson
*/
@autoserialize @autoserialize
public netid: string; public netid: string;
/**
* A string representing the last active date for this EPerson
*/
@autoserialize @autoserialize
public lastActive: string; public lastActive: string;
/**
* A boolean representing if this EPerson can log in
*/
@autoserialize @autoserialize
public canLogIn: boolean; public canLogIn: boolean;
/**
* The EPerson email address
*/
@autoserialize @autoserialize
public email: string; public email: string;
/**
* A boolean representing if this EPerson require certificate
*/
@autoserialize @autoserialize
public requireCertificate: boolean; public requireCertificate: boolean;
/**
* A boolean representing if this EPerson registered itself
*/
@autoserialize @autoserialize
public selfRegistered: boolean; public selfRegistered: boolean;
} }

View File

@@ -1,23 +1,38 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { mapsTo } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { Group } from './group.model'; import { Group } from './group.model';
import { ResourceType } from '../../shared/resource-type';
@mapsTo(Group) @mapsTo(Group)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedGroup extends NormalizedDSpaceObject<Group> implements CacheableObject, ListableObject { export class NormalizedGroup extends NormalizedDSpaceObject<Group> implements CacheableObject, ListableObject {
@autoserializeAs(NormalizedGroup) /**
groups: Group[]; * List of Groups that this Group belong to
*/
@deserialize
@relationship(ResourceType.Group, true)
groups: string[];
/**
* A string representing the unique handle of this Group
*/
@autoserialize @autoserialize
public handle: string; public handle: string;
/**
* A string representing the name of this Group
*/
@autoserialize @autoserialize
public name: string; public name: string;
/**
* A string representing the name of this Group is permanent
*/
@autoserialize @autoserialize
public permanent: boolean; public permanent: boolean;
} }

View File

@@ -1,29 +1,8 @@
// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts' import { MemoizedSelector } from '@ngrx/store';
import { createSelector, MemoizedSelector, Selector } from '@ngrx/store';
import { hasValue } from '../../shared/empty.util';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer'; import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
import { keySelector, subStateSelector } from '../../submission/selectors';
export function keySelector<T, V>(parentSelector: Selector<any, any>, subState: string, key: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => {
if (hasValue(state[subState])) {
return state[subState][key];
} else {
return undefined;
}
});
}
export function subStateSelector<T, V>(parentSelector: Selector<any, any>, subState: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => {
if (hasValue(state[subState])) {
return state[subState];
} else {
return undefined;
}
});
}
/** /**
* Return MemoizedSelector to select all jsonPatchOperations for a specified resource type, stored in the state * Return MemoizedSelector to select all jsonPatchOperations for a specified resource type, stored in the state

View File

@@ -1,4 +0,0 @@
import { Workspaceitem } from './workspaceitem.model';
export class EditItem extends Workspaceitem {
}

View File

@@ -1,10 +0,0 @@
import { inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { NormalizedSubmissionObject } from './normalized-submission-object.model';
import { EditItem } from './edititem.model';
@mapsTo(EditItem)
@inheritSerialization(NormalizedSubmissionObject)
export class NormalizedEditItem extends NormalizedSubmissionObject<EditItem> {
}

View File

@@ -5,22 +5,37 @@ import { Workflowitem } from './workflowitem.model';
import { NormalizedSubmissionObject } from './normalized-submission-object.model'; import { NormalizedSubmissionObject } from './normalized-submission-object.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
/**
* An model class for a NormalizedWorkflowItem.
*/
@mapsTo(Workflowitem) @mapsTo(Workflowitem)
@inheritSerialization(NormalizedSubmissionObject) @inheritSerialization(NormalizedSubmissionObject)
export class NormalizedWorkflowItem extends NormalizedSubmissionObject<Workflowitem> { export class NormalizedWorkflowItem extends NormalizedSubmissionObject<Workflowitem> {
/**
* The collection this workflowitem belonging to
*/
@autoserialize @autoserialize
@relationship(ResourceType.Collection, false) @relationship(ResourceType.Collection, false)
collection: string; collection: string;
/**
* The item created with this workflowitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.Item, false) @relationship(ResourceType.Item, false)
item: string; item: string;
/**
* The configuration object that define this workflowitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.SubmissionDefinition, false) @relationship(ResourceType.SubmissionDefinition, false)
submissionDefinition: string; submissionDefinition: string;
/**
* The EPerson who submit this workflowitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.EPerson, false) @relationship(ResourceType.EPerson, false)
submitter: string; submitter: string;

View File

@@ -7,23 +7,38 @@ import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-obj
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { Workflowitem } from './workflowitem.model'; import { Workflowitem } from './workflowitem.model';
/**
* An model class for a NormalizedWorkspaceItem.
*/
@mapsTo(Workspaceitem) @mapsTo(Workspaceitem)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
@inheritSerialization(NormalizedSubmissionObject) @inheritSerialization(NormalizedSubmissionObject)
export class NormalizedWorkspaceItem extends NormalizedSubmissionObject<Workflowitem> { export class NormalizedWorkspaceItem extends NormalizedSubmissionObject<Workflowitem> {
/**
* The collection this workspaceitem belonging to
*/
@autoserialize @autoserialize
@relationship(ResourceType.Collection, false) @relationship(ResourceType.Collection, false)
collection: string; collection: string;
/**
* The item created with this workspaceitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.Item, false) @relationship(ResourceType.Item, false)
item: string; item: string;
/**
* The configuration object that define this workspaceitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.SubmissionDefinition, false) @relationship(ResourceType.SubmissionDefinition, false)
submissionDefinition: string; submissionDefinition: string;
/**
* The EPerson who submit this workspaceitem
*/
@autoserialize @autoserialize
@relationship(ResourceType.EPerson, false) @relationship(ResourceType.EPerson, false)
submitter: string; submitter: string;

View File

@@ -46,7 +46,7 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable
sections: WorkspaceitemSectionsObject; sections: WorkspaceitemSectionsObject;
/** /**
* The submission config definition * The configuration object that define this submission
*/ */
submissionDefinition: Observable<RemoteData<SubmissionDefinitionsModel>> | SubmissionDefinitionsModel; submissionDefinition: Observable<RemoteData<SubmissionDefinitionsModel>> | SubmissionDefinitionsModel;

View File

@@ -1,7 +1,30 @@
/**
* An interface to represent bitstream's access condition.
*/
export class SubmissionUploadFileAccessConditionObject { export class SubmissionUploadFileAccessConditionObject {
/**
* The access condition id
*/
id: string; id: string;
/**
* The access condition name
*/
name: string; name: string;
/**
* The access group UUID defined in this access condition
*/
groupUUID: string; groupUUID: string;
/**
* Possible start date of the access condition
*/
startDate: string; startDate: string;
/**
* Possible end date of the access condition
*/
endDate: string; endDate: string;
} }

View File

@@ -1,4 +1,7 @@
import { Workspaceitem } from './workspaceitem.model'; import { Workspaceitem } from './workspaceitem.model';
/**
* A model class for a Workflowitem.
*/
export class Workflowitem extends Workspaceitem { export class Workflowitem extends Workspaceitem {
} }

View File

@@ -1,21 +0,0 @@
import { Item } from '../../shared/item.model';
export interface WorkspaceitemSectionDetectDuplicateObject {
matches: {
[itemId: string]: DetectDuplicateMatch;
};
}
export interface DetectDuplicateMatch {
submitterDecision?: string; // [reject|verify]
submitterNote?: string;
submitterTime?: string; // (readonly)
workflowDecision?: string; // [reject|verify]
workflowNote?: string;
workflowTime?: string; // (readonly)
adminDecision?: string;
matchObject?: Item;
}

View File

@@ -1,6 +1,10 @@
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { MetadataMapInterface } from '../../shared/metadata.models'; import { MetadataMapInterface } from '../../shared/metadata.models';
/**
* An interface to represent submission's form section data.
* A map of metadata keys to an ordered list of FormFieldMetadataValueObject objects.
*/
export interface WorkspaceitemSectionFormObject extends MetadataMapInterface { export interface WorkspaceitemSectionFormObject extends MetadataMapInterface {
[metadata: string]: FormFieldMetadataValueObject[]; [metadata: string]: FormFieldMetadataValueObject[];
} }

View File

@@ -1,5 +1,20 @@
/**
* An interface to represent submission's license section data.
*/
export interface WorkspaceitemSectionLicenseObject { export interface WorkspaceitemSectionLicenseObject {
/**
* The license url
*/
url: string; url: string;
/**
* The acceptance date of the license
*/
acceptanceDate: string; acceptanceDate: string;
/**
* A boolean representing if license has been granted
*/
granted: boolean; granted: boolean;
} }

View File

@@ -1,8 +0,0 @@
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
export interface WorkspaceitemSectionRecycleObject {
unexpected: any;
metadata: FormFieldMetadataValueObject[];
files: WorkspaceitemSectionUploadFileObject[];
}

View File

@@ -1,15 +1,46 @@
import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model'; import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model';
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
/**
* An interface to represent submission's upload section file entry.
*/
export class WorkspaceitemSectionUploadFileObject { export class WorkspaceitemSectionUploadFileObject {
/**
* The file UUID
*/
uuid: string; uuid: string;
/**
* The file metadata
*/
metadata: WorkspaceitemSectionFormObject; metadata: WorkspaceitemSectionFormObject;
/**
* The file size
*/
sizeBytes: number; sizeBytes: number;
/**
* The file check sum
*/
checkSum: { checkSum: {
checkSumAlgorithm: string; checkSumAlgorithm: string;
value: string; value: string;
}; };
/**
* The file url
*/
url: string; url: string;
/**
* The file thumbnail url
*/
thumbnail: string; thumbnail: string;
/**
* The list of file access conditions
*/
accessConditions: SubmissionUploadFileAccessConditionObject[]; accessConditions: SubmissionUploadFileAccessConditionObject[];
} }

View File

@@ -1,5 +1,12 @@
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model'; import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
/**
* An interface to represent submission's upload section data.
*/
export interface WorkspaceitemSectionUploadObject { export interface WorkspaceitemSectionUploadObject {
/**
* A list of [[WorkspaceitemSectionUploadFileObject]]
*/
files: WorkspaceitemSectionUploadFileObject[]; files: WorkspaceitemSectionUploadFileObject[];
} }

View File

@@ -1,17 +1,20 @@
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
import { WorkspaceitemSectionRecycleObject } from './workspaceitem-section-recycle.model';
import { WorkspaceitemSectionDetectDuplicateObject } from './workspaceitem-section-deduplication.model';
/**
* An interface to represent submission's section object.
* A map of section keys to an ordered list of WorkspaceitemSectionDataType objects.
*/
export class WorkspaceitemSectionsObject { export class WorkspaceitemSectionsObject {
[name: string]: WorkspaceitemSectionDataType; [name: string]: WorkspaceitemSectionDataType;
} }
/**
* Export a type alias of all sections
*/
export type WorkspaceitemSectionDataType export type WorkspaceitemSectionDataType
= WorkspaceitemSectionUploadObject = WorkspaceitemSectionUploadObject
| WorkspaceitemSectionFormObject | WorkspaceitemSectionFormObject
| WorkspaceitemSectionLicenseObject | WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionRecycleObject
| WorkspaceitemSectionDetectDuplicateObject
| string; | string;

View File

@@ -1,5 +1,8 @@
import { SubmissionObject } from './submission-object.model'; import { SubmissionObject } from './submission-object.model';
/**
* A model class for a Workspaceitem.
*/
export class Workspaceitem extends SubmissionObject { export class Workspaceitem extends SubmissionObject {
} }

View File

@@ -9,6 +9,9 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon
import { SubmissionPatchRequest } from '../data/request.models'; import { SubmissionPatchRequest } from '../data/request.models';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
/**
* A service that provides methods to make JSON Patch requests.
*/
@Injectable() @Injectable()
export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> { export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
protected linkPath = ''; protected linkPath = '';

View File

@@ -11,7 +11,6 @@ export enum SubmissionResourceType {
Group = 'group', Group = 'group',
WorkspaceItem = 'workspaceitem', WorkspaceItem = 'workspaceitem',
WorkflowItem = 'workflowitem', WorkflowItem = 'workflowitem',
EditItem = 'edititem',
SubmissionDefinitions = 'submissiondefinitions', SubmissionDefinitions = 'submissiondefinitions',
SubmissionDefinition = 'submissiondefinition', SubmissionDefinition = 'submissiondefinition',
SubmissionForm = 'submissionform', SubmissionForm = 'submissionform',

View File

@@ -13,11 +13,15 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { SubmissionResourceType } from './submission-resource-type'; import { SubmissionResourceType } from './submission-resource-type';
import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model';
import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
import { NormalizedEditItem } from './models/normalized-edititem.model';
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
import { SubmissionObject } from './models/submission-object.model'; import { SubmissionObject } from './models/submission-object.model';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
/**
* Export a function to check if object has same properties of FormFieldMetadataValueObject
*
* @param obj
*/
export function isServerFormValue(obj: any): boolean { export function isServerFormValue(obj: any): boolean {
return (typeof obj === 'object' return (typeof obj === 'object'
&& obj.hasOwnProperty('value') && obj.hasOwnProperty('value')
@@ -27,6 +31,11 @@ export function isServerFormValue(obj: any): boolean {
&& obj.hasOwnProperty('place')) && obj.hasOwnProperty('place'))
} }
/**
* Export a function to normalize sections object of the server response
*
* @param obj
*/
export function normalizeSectionData(obj: any) { export function normalizeSectionData(obj: any) {
let result: any = obj; let result: any = obj;
if (isNotNull(obj)) { if (isNotNull(obj)) {
@@ -74,6 +83,13 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
super(); super();
} }
/**
* Parses data from the workspaceitems/workflowitems endpoints
*
* @param {RestRequest} request
* @param {DSpaceRESTV2Response} data
* @returns {RestResponse}
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) if (isNotEmpty(data.payload)
&& isNotEmpty(data.payload._links) && isNotEmpty(data.payload._links)
@@ -93,6 +109,13 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
} }
} }
/**
* Parses response and normalize it
*
* @param {DSpaceRESTV2Response} data
* @param {string} requestHref
* @returns {any[]}
*/
protected processResponse<ObjectDomain, ObjectType>(data: any, requestHref: string): any[] { protected processResponse<ObjectDomain, ObjectType>(data: any, requestHref: string): any[] {
const dataDefinition = this.process<ObjectDomain, ObjectType>(data, requestHref); const dataDefinition = this.process<ObjectDomain, ObjectType>(data, requestHref);
const normalizedDefinition = Array.of(); const normalizedDefinition = Array.of();
@@ -103,8 +126,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
let normalizedItem = Object.assign({}, item); let normalizedItem = Object.assign({}, item);
// In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form // In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form
if (item instanceof NormalizedWorkspaceItem if (item instanceof NormalizedWorkspaceItem
|| item instanceof NormalizedWorkflowItem || item instanceof NormalizedWorkflowItem) {
|| item instanceof NormalizedEditItem) {
if (item.sections) { if (item.sections) {
const precessedSection = Object.create({}); const precessedSection = Object.create({});
// Iterate over all workspaceitem's sections // Iterate over all workspaceitem's sections

View File

@@ -2,18 +2,18 @@ import { TestScheduler } from 'rxjs/testing';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { SubmissionRestService } from './submission-rest.service'; import { SubmissionRestService } from './submission-rest.service';
import { RequestService } from '../core/data/request.service'; import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getMockRequestService } from '../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockRemoteDataBuildService } from '../shared/mocks/mock-remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { import {
SubmissionDeleteRequest, SubmissionDeleteRequest,
SubmissionPatchRequest, SubmissionPatchRequest,
SubmissionPostRequest, SubmissionPostRequest,
SubmissionRequest SubmissionRequest
} from '../core/data/request.models'; } from '../data/request.models';
import { FormFieldMetadataValueObject } from '../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
describe('SubmissionRestService test suite', () => { describe('SubmissionRestService test suite', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;

View File

@@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators';
import { RequestService } from '../core/data/request.service'; import { RequestService } from '../data/request.service';
import { isNotEmpty } from '../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { import {
DeleteRequest, DeleteRequest,
PostRequest, PostRequest,
@@ -13,14 +13,17 @@ import {
SubmissionPatchRequest, SubmissionPatchRequest,
SubmissionPostRequest, SubmissionPostRequest,
SubmissionRequest SubmissionRequest
} from '../core/data/request.models'; } from '../data/request.models';
import { SubmitDataResponseDefinitionObject } from '../core/shared/submit-data-response-definition.model'; import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HALEndpointService } from '../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../core/cache/response.models'; import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
import { getResponseFromEntry } from '../core/shared/operators'; import { getResponseFromEntry } from '../shared/operators';
/**
* The service handling all submission REST requests
*/
@Injectable() @Injectable()
export class SubmissionRestService { export class SubmissionRestService {
protected linkPath = 'workspaceitems'; protected linkPath = 'workspaceitems';
@@ -31,6 +34,14 @@ export class SubmissionRestService {
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
} }
/**
* Fetch a RestRequest
*
* @param requestId
* The base endpoint for the type of object
* @return Observable<SubmitDataResponseDefinitionObject>
* server response
*/
protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> { protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> {
const responses = this.requestService.getByUUID(requestId).pipe( const responses = this.requestService.getByUUID(requestId).pipe(
getResponseFromEntry() getResponseFromEntry()
@@ -47,10 +58,28 @@ export class SubmissionRestService {
return observableMerge(errorResponses, successResponses); return observableMerge(errorResponses, successResponses);
} }
/**
* Create the HREF for a specific submission object based on its identifier
*
* @param endpoint
* The base endpoint for the type of object
* @param resourceID
* The identifier for the object
*/
protected getEndpointByIDHref(endpoint, resourceID): string { protected getEndpointByIDHref(endpoint, resourceID): string {
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
} }
/**
* Delete an existing submission Object on the server
*
* @param scopeId
* The submission Object to be removed
* @param linkName
* The endpoint link name
* @return Observable<SubmitDataResponseDefinitionObject>
* server response
*/
public deleteById(scopeId: string, linkName?: string): Observable<SubmitDataResponseDefinitionObject> { public deleteById(scopeId: string, linkName?: string): Observable<SubmitDataResponseDefinitionObject> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkName || this.linkPath).pipe( return this.halService.getEndpoint(linkName || this.linkPath).pipe(
@@ -59,11 +88,21 @@ export class SubmissionRestService {
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)), map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
map((endpointURL: string) => new SubmissionDeleteRequest(requestId, endpointURL)), map((endpointURL: string) => new SubmissionDeleteRequest(requestId, endpointURL)),
tap((request: DeleteRequest) => this.requestService.configure(request)), tap((request: DeleteRequest) => this.requestService.configure(request)),
flatMap((request: DeleteRequest) => this.fetchRequest(requestId)), flatMap(() => this.fetchRequest(requestId)),
distinctUntilChanged()); distinctUntilChanged());
} }
public getDataById(linkName: string, id: string): Observable<any> { /**
* Return an existing submission Object from the server
*
* @param linkName
* The endpoint link name
* @param id
* The submission Object to retrieve
* @return Observable<SubmitDataResponseDefinitionObject>
* server response
*/
public getDataById(linkName: string, id: string): Observable<SubmitDataResponseDefinitionObject> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkName).pipe( return this.halService.getEndpoint(linkName).pipe(
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)), map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
@@ -71,10 +110,24 @@ export class SubmissionRestService {
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)), map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
tap((request: RestRequest) => this.requestService.configure(request, true)), tap((request: RestRequest) => this.requestService.configure(request, true)),
flatMap((request: RestRequest) => this.fetchRequest(requestId)), flatMap(() => this.fetchRequest(requestId)),
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Make a new post request
*
* @param linkName
* The endpoint link name
* @param body
* The post request body
* @param scopeId
* The submission Object id
* @param options
* The [HttpOptions] object
* @return Observable<SubmitDataResponseDefinitionObject>
* server response
*/
public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable<SubmitDataResponseDefinitionObject> { public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable<SubmitDataResponseDefinitionObject> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkName).pipe( return this.halService.getEndpoint(linkName).pipe(
@@ -83,10 +136,22 @@ export class SubmissionRestService {
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)), map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)),
tap((request: PostRequest) => this.requestService.configure(request)), tap((request: PostRequest) => this.requestService.configure(request)),
flatMap((request: PostRequest) => this.fetchRequest(requestId)), flatMap(() => this.fetchRequest(requestId)),
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Make a new patch to a specified object
*
* @param linkName
* The endpoint link name
* @param body
* The post request body
* @param scopeId
* The submission Object id
* @return Observable<SubmitDataResponseDefinitionObject>
* server response
*/
public patchToEndpoint(linkName: string, body: any, scopeId?: string): Observable<SubmitDataResponseDefinitionObject> { public patchToEndpoint(linkName: string, body: any, scopeId?: string): Observable<SubmitDataResponseDefinitionObject> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkName).pipe( return this.halService.getEndpoint(linkName).pipe(
@@ -95,7 +160,7 @@ export class SubmissionRestService {
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new SubmissionPatchRequest(requestId, endpointURL, body)), map((endpointURL: string) => new SubmissionPatchRequest(requestId, endpointURL, body)),
tap((request: PostRequest) => this.requestService.configure(request)), tap((request: PostRequest) => this.requestService.configure(request)),
flatMap((request: PostRequest) => this.fetchRequest(requestId)), flatMap(() => this.fetchRequest(requestId)),
distinctUntilChanged()); distinctUntilChanged());
} }

View File

@@ -1,5 +1,4 @@
export enum SubmissionScopeType { export enum SubmissionScopeType {
WorkspaceItem = 'WORKSPACE', WorkspaceItem = 'WORKSPACE',
WorkflowItem = 'WORKFLOW', WorkflowItem = 'WORKFLOW'
EditItem = 'ITEM',
} }

View File

@@ -14,6 +14,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
/**
* A service that provides methods to make REST requests with workflowitems endpoint.
*/
@Injectable() @Injectable()
export class WorkflowitemDataService extends DataService<Workflowitem> { export class WorkflowitemDataService extends DataService<Workflowitem> {
protected linkPath = 'workflowitems'; protected linkPath = 'workflowitems';

View File

@@ -14,6 +14,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
/**
* A service that provides methods to make REST requests with workspaceitems endpoint.
*/
@Injectable() @Injectable()
export class WorkspaceitemDataService extends DataService<Workspaceitem> { export class WorkspaceitemDataService extends DataService<Workspaceitem> {
protected linkPath = 'workspaceitems'; protected linkPath = 'workspaceitems';

View File

@@ -2,6 +2,9 @@ import { ServerResponseService } from '../shared/services/server-response.servic
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
/**
* This component representing the `PageNotFound` DSpace page.
*/
@Component({ @Component({
selector: 'ds-pagenotfound', selector: 'ds-pagenotfound',
styleUrls: ['./pagenotfound.component.scss'], styleUrls: ['./pagenotfound.component.scss'],
@@ -9,10 +12,20 @@ import { AuthService } from '../core/auth/auth.service';
changeDetection: ChangeDetectionStrategy.Default changeDetection: ChangeDetectionStrategy.Default
}) })
export class PageNotFoundComponent implements OnInit { export class PageNotFoundComponent implements OnInit {
/**
* Initialize instance variables
*
* @param {AuthService} authservice
* @param {ServerResponseService} responseService
*/
constructor(private authservice: AuthService, private responseService: ServerResponseService) { constructor(private authservice: AuthService, private responseService: ServerResponseService) {
this.responseService.setNotFound(); this.responseService.setNotFound();
} }
/**
* Remove redirect url from the state
*/
ngOnInit(): void { ngOnInit(): void {
this.authservice.clearRedirectUrl(); this.authservice.clearRedirectUrl();
} }

View File

@@ -0,0 +1,114 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { BrowserModule, By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { AlertComponent } from './alert.component';
import { createTestComponent } from '../testing/utils';
import { AlertType } from './aletr-type';
describe('AlertComponent test suite', () => {
let comp: AlertComponent;
let compAsAny: any;
let fixture: ComponentFixture<AlertComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserModule,
CommonModule,
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [
AlertComponent,
TestComponent
],
providers: [
ChangeDetectorRef,
AlertComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-alert [content]="content" [dismissible]="dismissible" [type]="type"></ds-alert>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create AlertComponent', inject([AlertComponent], (app: AlertComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AlertComponent);
comp = fixture.componentInstance;
compAsAny = comp;
comp.content = 'test alert';
comp.dismissible = true;
comp.type = AlertType.Info;
fixture.detectChanges();
});
it('should display close icon when dismissible is true', () => {
const btn = fixture.debugElement.query(By.css('.close'));
expect(btn).toBeDefined();
});
it('should not display close icon when dismissible is false', () => {
comp.dismissible = false;
fixture.detectChanges();
const btn = fixture.debugElement.query(By.css('.close'));
expect(btn).toBeDefined();
});
it('should dismiss alert when click on close icon', () => {
spyOn(comp, 'dismiss');
const btn = fixture.debugElement.query(By.css('.close'));
btn.nativeElement.click();
expect(comp.dismiss).toHaveBeenCalled();
});
afterEach(() => {
fixture.destroy();
comp = null;
compAsAny = null;
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
content = 'test alert';
dismissible = true;
type = AlertType.Info;
}

View File

@@ -1,9 +1,12 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { trigger } from '@angular/animations'; import { trigger } from '@angular/animations';
import { AlertType } from './aletrs-type'; import { AlertType } from './aletr-type';
import { fadeOutLeave, fadeOutState } from '../animations/fade'; import { fadeOutLeave, fadeOutState } from '../animations/fade';
/**
* This component allow to create div that uses the Bootstrap's Alerts component.
*/
@Component({ @Component({
selector: 'ds-alert', selector: 'ds-alert',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
@@ -12,23 +15,52 @@ import { fadeOutLeave, fadeOutState } from '../animations/fade';
fadeOutLeave, fadeOutState, fadeOutLeave, fadeOutState,
]) ])
], ],
templateUrl: './alerts.component.html', templateUrl: './alert.component.html',
styleUrls: ['./alerts.component.scss'] styleUrls: ['./alert.component.scss']
}) })
export class AlertComponent {
export class AlertsComponent { /**
* The alert content
*/
@Input() content: string; @Input() content: string;
/**
* A boolean representing if alert is dismissible
*/
@Input() dismissible = false; @Input() dismissible = false;
/**
* The alert type
*/
@Input() type: AlertType; @Input() type: AlertType;
/**
* An event fired when alert is dismissed.
*/
@Output() close: EventEmitter<any> = new EventEmitter<any>(); @Output() close: EventEmitter<any> = new EventEmitter<any>();
/**
* The initial animation name
*/
public animate = 'fadeIn'; public animate = 'fadeIn';
/**
* A boolean representing if alert is dismissed or not
*/
public dismissed = false; public dismissed = false;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
*/
constructor(private cdr: ChangeDetectorRef) { constructor(private cdr: ChangeDetectorRef) {
} }
/**
* Dismiss div with animation
*/
dismiss() { dismiss() {
if (this.dismissible) { if (this.dismissible) {
this.animate = 'fadeOut'; this.animate = 'fadeOut';

View File

@@ -19,25 +19,55 @@ import { isNotEmpty, isNull } from '../empty.util';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { ConfidenceIconConfig } from '../../../config/submission-config.interface'; import { ConfidenceIconConfig } from '../../../config/submission-config.interface';
/**
* Directive to add to the element a bootstrap utility class based on metadata confidence value
*/
@Directive({ @Directive({
selector: '[dsAuthorityConfidenceState]' selector: '[dsAuthorityConfidenceState]'
}) })
export class AuthorityConfidenceStateDirective implements OnChanges { export class AuthorityConfidenceStateDirective implements OnChanges {
/**
* The metadata value
*/
@Input() authorityValue: AuthorityValue | FormFieldMetadataValueObject | string; @Input() authorityValue: AuthorityValue | FormFieldMetadataValueObject | string;
/**
* A boolean representing if to show html icon if authority value is empty
*/
@Input() visibleWhenAuthorityEmpty = true; @Input() visibleWhenAuthorityEmpty = true;
/**
* The css class applied before directive changes
*/
private previousClass: string = null; private previousClass: string = null;
/**
* The css class applied after directive changes
*/
private newClass: string; private newClass: string;
/**
* An event fired when click on element that has a confidence value empty or different from CF_ACCEPTED
*/
@Output() whenClickOnConfidenceNotAccepted: EventEmitter<ConfidenceType> = new EventEmitter<ConfidenceType>(); @Output() whenClickOnConfidenceNotAccepted: EventEmitter<ConfidenceType> = new EventEmitter<ConfidenceType>();
/**
* Listener to click event
*/
@HostListener('click') onClick() { @HostListener('click') onClick() {
if (isNotEmpty(this.authorityValue) && this.getConfidenceByValue(this.authorityValue) !== ConfidenceType.CF_ACCEPTED) { if (isNotEmpty(this.authorityValue) && this.getConfidenceByValue(this.authorityValue) !== ConfidenceType.CF_ACCEPTED) {
this.whenClickOnConfidenceNotAccepted.emit(this.getConfidenceByValue(this.authorityValue)); this.whenClickOnConfidenceNotAccepted.emit(this.getConfidenceByValue(this.authorityValue));
} }
} }
/**
* Initialize instance variables
*
* @param {GlobalConfig} EnvConfig
* @param {ElementRef} elem
* @param {Renderer2} renderer
*/
constructor( constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private elem: ElementRef, private elem: ElementRef,
@@ -45,6 +75,11 @@ export class AuthorityConfidenceStateDirective implements OnChanges {
) { ) {
} }
/**
* Apply css class to element whenever authority value change
*
* @param {SimpleChanges} changes
*/
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (!changes.authorityValue.firstChange) { if (!changes.authorityValue.firstChange) {
this.previousClass = this.getClassByConfidence(this.getConfidenceByValue(changes.authorityValue.previousValue)) this.previousClass = this.getClassByConfidence(this.getConfidenceByValue(changes.authorityValue.previousValue))
@@ -59,6 +94,9 @@ export class AuthorityConfidenceStateDirective implements OnChanges {
} }
} }
/**
* Apply css class to element after view init
*/
ngAfterViewInit() { ngAfterViewInit() {
if (isNull(this.previousClass)) { if (isNull(this.previousClass)) {
this.renderer.addClass(this.elem.nativeElement, this.newClass); this.renderer.addClass(this.elem.nativeElement, this.newClass);
@@ -68,6 +106,11 @@ export class AuthorityConfidenceStateDirective implements OnChanges {
} }
} }
/**
* Return confidence value as ConfidenceType
*
* @param value
*/
private getConfidenceByValue(value: any): ConfidenceType { private getConfidenceByValue(value: any): ConfidenceType {
let confidence: ConfidenceType = ConfidenceType.CF_UNSET; let confidence: ConfidenceType = ConfidenceType.CF_UNSET;
@@ -82,6 +125,11 @@ export class AuthorityConfidenceStateDirective implements OnChanges {
return confidence; return confidence;
} }
/**
* Return the properly css class based on confidence value
*
* @param confidence
*/
private getClassByConfidence(confidence: any): string { private getClassByConfidence(confidence: any): string {
if (!this.visibleWhenAuthorityEmpty && confidence === ConfidenceType.CF_UNSET) { if (!this.visibleWhenAuthorityEmpty && confidence === ConfidenceType.CF_UNSET) {
return 'd-none'; return 'd-none';

View File

@@ -131,7 +131,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
} }
@Component({ @Component({
selector: 'ds-dynamic-form-control', selector: 'ds-dynamic-form-control-container',
styleUrls: ['./ds-dynamic-form-control-container.component.scss'], styleUrls: ['./ds-dynamic-form-control-container.component.scss'],
templateUrl: './ds-dynamic-form-control-container.component.html', templateUrl: './ds-dynamic-form-control-container.component.html',
changeDetection: ChangeDetectionStrategy.Default changeDetection: ChangeDetectionStrategy.Default
@@ -180,9 +180,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
if (changes) { if (changes) {
super.ngOnChanges(changes); super.ngOnChanges(changes);
if (this.model && this.model.placeholder) { if (this.model && this.model.placeholder) {
this.translateService.get(this.model.placeholder).subscribe((placeholder) => { this.model.placeholder = this.translateService.instant(this.model.placeholder);
this.model.placeholder = placeholder;
})
} }
} }
} }

View File

@@ -1,12 +1,12 @@
<ds-dynamic-form-control *ngFor="let model of formModel; trackBy: trackByFn" <ds-dynamic-form-control-container *ngFor="let model of formModel; trackBy: trackByFn"
[formId]="formId" [formId]="formId"
[group]="formGroup" [group]="formGroup"
[hasErrorMessaging]="model.hasErrorMessages" [hasErrorMessaging]="model.hasErrorMessages"
[hidden]="model.hidden" [hidden]="model.hidden"
[layout]="formLayout" [layout]="formLayout"
[model]="model" [model]="model"
[ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]" [ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]"
[templates]="templates" [templates]="templates"
(dfBlur)="onEvent($event, 'blur')" (dfBlur)="onEvent($event, 'blur')"
(dfChange)="onEvent($event, 'change')" (dfChange)="onEvent($event, 'change')"
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control> (dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control-container>

View File

@@ -9,7 +9,7 @@
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container> <ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
<ds-dynamic-form-control *ngFor="let _model of groupModel.group" <ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"
[bindId]="false" [bindId]="false"
[context]="groupModel" [context]="groupModel"
[group]="control.at(idx)" [group]="control.at(idx)"
@@ -21,7 +21,7 @@
(dfBlur)="onBlur($event)" (dfBlur)="onBlur($event)"
(dfChange)="onChange($event)" (dfChange)="onChange($event)"
(dfFocus)="onFocus($event)" (dfFocus)="onFocus($event)"
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control> (ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container> <ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>

View File

@@ -10,7 +10,7 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
@Component({ @Component({
selector: 'ds-date-picker-inline', selector: 'ds-dynamic-date-picker-inline',
templateUrl: './dynamic-date-picker-inline.component.html' templateUrl: './dynamic-date-picker-inline.component.html'
}) })
export class DsDatePickerInlineComponent extends DynamicFormControlComponent { export class DsDatePickerInlineComponent extends DynamicFormControlComponent {

View File

@@ -5,7 +5,7 @@
[formGroupName]="model.id" [formGroupName]="model.id"
[ngClass]="getClass('element','control')"> [ngClass]="getClass('element','control')">
<ds-dynamic-form-control *ngFor="let _model of model.group" <ds-dynamic-form-control-container *ngFor="let _model of model.group"
[asBootstrapFormGroup]="true" [asBootstrapFormGroup]="true"
[formId]="formId" [formId]="formId"
[group]="control" [group]="control"
@@ -18,7 +18,7 @@
(dfBlur)="onBlur($event)" (dfBlur)="onBlur($event)"
(dfChange)="onChange($event)" (dfChange)="onChange($event)"
(dfFocus)="onFocus($event)" (dfFocus)="onFocus($event)"
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control> (ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
</div> </div>
</ng-container> </ng-container>

View File

@@ -1,6 +1,6 @@
import { map, distinctUntilChanged, filter } from 'rxjs/operators'; import { map, distinctUntilChanged, filter } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
@@ -82,12 +82,13 @@ export class FormService {
/** /**
* Method to validate form's fields * Method to validate form's fields
*/ */
public validateAllFormFields(formGroup: FormGroup) { public validateAllFormFields(formGroup: FormGroup | FormArray) {
Object.keys(formGroup.controls).forEach((field) => { Object.keys(formGroup.controls).forEach((field) => {
const control = formGroup.get(field); const control = formGroup.get(field);
if (control instanceof FormControl) { if (control instanceof FormControl) {
control.markAsTouched({ onlySelf: true }); control.markAsTouched({ onlySelf: true });
} else if (control instanceof FormGroup) { control.markAsDirty({ onlySelf: true });
} else if (control instanceof FormGroup || control instanceof FormArray) {
this.validateAllFormFields(control); this.validateAllFormFields(control);
} }
}); });

View File

@@ -1,5 +1,8 @@
import { SectionFormOperationsService } from '../../submission/sections/form/section-form-operations.service'; import { SectionFormOperationsService } from '../../submission/sections/form/section-form-operations.service';
/**
* Mock for [[FormOperationsService]]
*/
export function getMockFormOperationsService(): SectionFormOperationsService { export function getMockFormOperationsService(): SectionFormOperationsService {
return jasmine.createSpyObj('SectionFormOperationsService', { return jasmine.createSpyObj('SectionFormOperationsService', {
dispatchOperationsFromEvent: jasmine.createSpy('dispatchOperationsFromEvent'), dispatchOperationsFromEvent: jasmine.createSpy('dispatchOperationsFromEvent'),

View File

@@ -2,6 +2,9 @@ import { of as observableOf } from 'rxjs';
import { FormService } from '../form/form.service'; import { FormService } from '../form/form.service';
/**
* Mock for [[FormService]]
*/
export function getMockFormService( export function getMockFormService(
id$: string = 'random_id' id$: string = 'random_id'
): FormService { ): FormService {
@@ -12,8 +15,8 @@ export function getMockFormService(
getForm: observableOf({}), getForm: observableOf({}),
getUniqueId: id$, getUniqueId: id$,
resetForm: {}, resetForm: {},
validateAllFormFields: {}, validateAllFormFields: jasmine.createSpy('validateAllFormFields'),
isValid: observableOf(true), isValid: jasmine.createSpy('isValid'),
isFormInitialized: observableOf(true) isFormInitialized: observableOf(true)
}); });

View File

@@ -1,5 +1,8 @@
import { Observable, of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
/**
* Mock for [[RouterService]]
*/
export class MockRouter { export class MockRouter {
public events = observableOf({}); public events = observableOf({});
public routerState = { public routerState = {

View File

@@ -1,5 +1,8 @@
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
/**
* Mock for [[ScrollToService]]
*/
export function getMockScrollToService(): ScrollToService { export function getMockScrollToService(): ScrollToService {
return jasmine.createSpyObj('scrollToService', { return jasmine.createSpyObj('scrollToService', {
scrollTo: jasmine.createSpy('scrollTo') scrollTo: jasmine.createSpy('scrollTo')

View File

@@ -1,5 +1,8 @@
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service'; import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
/**
* Mock for [[SubmissionFormsConfigService]]
*/
export function getMockSectionUploadService(): SubmissionFormsConfigService { export function getMockSectionUploadService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SectionUploadService', { return jasmine.createSpyObj('SectionUploadService', {
getUploadedFileList: jasmine.createSpy('getUploadedFileList'), getUploadedFileList: jasmine.createSpy('getUploadedFileList'),

View File

@@ -3,6 +3,7 @@ import { SubmissionDefinitionsModel } from '../../core/config/models/config-subm
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { Group } from '../../core/eperson/models/group.model';
export const mockSectionsData = { export const mockSectionsData = {
traditionalpageone:{ traditionalpageone:{
@@ -1364,7 +1365,7 @@ export const mockAccessConditionOptions = [
} }
]; ];
export const mockGroup = { export const mockGroup = Object.assign(new Group(), {
handle: null, handle: null,
permanent: true, permanent: true,
self: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1', self: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1',
@@ -1386,7 +1387,7 @@ export const mockGroup = {
}, },
page: [] page: []
} }
}; });
export const mockUploadFiles = [ export const mockUploadFiles = [
{ {

View File

@@ -76,7 +76,7 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service'; import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { AlertsComponent } from './alerts/alerts.component'; import { AlertComponent } from './alert/alert.component';
import { MyDSpaceResultListElementComponent } from './object-list/my-dspace-result-list-element/my-dspace-result-list-element.component'; import { MyDSpaceResultListElementComponent } from './object-list/my-dspace-result-list-element/my-dspace-result-list-element.component';
import { MessageBoardComponent } from './message-board/message-board.component'; import { MessageBoardComponent } from './message-board/message-board.component';
import { MessageComponent } from './message-board/message/message.component'; import { MessageComponent } from './message-board/message/message.component';
@@ -180,7 +180,7 @@ const PIPES = [
const COMPONENTS = [ const COMPONENTS = [
// put shared components here // put shared components here
AlertsComponent, AlertComponent,
AuthNavMenuComponent, AuthNavMenuComponent,
UserMenuComponent, UserMenuComponent,
ChipsComponent, ChipsComponent,

View File

@@ -1,5 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
/**
* Pipe that allows to iterate over an object and to access to entry key and value :
*
* <div *ngFor="let obj of objs | dsObjNgFor">
* {{obj.key}} - {{obj.value}}
* </div>
*
*/
@Pipe({ @Pipe({
name: 'dsObjNgFor' name: 'dsObjNgFor'
}) })

View File

@@ -1,7 +1,7 @@
<div class="submission-submit-container" > <div class="submission-submit-container" >
<ds-submission-submit-form [collectionId]="collectionId" <ds-submission-form [collectionId]="collectionId"
[sections]="sections" [sections]="sections"
[selfUrl]="selfUrl" [selfUrl]="selfUrl"
[submissionDefinition]="submissionDefinition" [submissionDefinition]="submissionDefinition"
[submissionId]="submissionId"></ds-submission-submit-form> [submissionId]="submissionId"></ds-submission-form>
</div> </div>

View File

@@ -14,17 +14,44 @@ import { SubmissionObject } from '../../core/submission/models/submission-object
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
/**
* This component allows to edit an existing workspaceitem/workflowitem.
*/
@Component({ @Component({
selector: 'ds-submission-edit', selector: 'ds-submission-edit',
styleUrls: ['./submission-edit.component.scss'], styleUrls: ['./submission-edit.component.scss'],
templateUrl: './submission-edit.component.html' templateUrl: './submission-edit.component.html'
}) })
export class SubmissionEditComponent implements OnDestroy, OnInit { export class SubmissionEditComponent implements OnDestroy, OnInit {
/**
* The collection id this submission belonging to
* @type {string}
*/
public collectionId: string; public collectionId: string;
/**
* The list of submission's sections
* @type {WorkspaceitemSectionsObject}
*/
public sections: WorkspaceitemSectionsObject; public sections: WorkspaceitemSectionsObject;
/**
* The submission self url
* @type {string}
*/
public selfUrl: string; public selfUrl: string;
/**
* The configuration object that define this submission
* @type {SubmissionDefinitionsModel}
*/
public submissionDefinition: SubmissionDefinitionsModel; public submissionDefinition: SubmissionDefinitionsModel;
/**
* The submission id
* @type {string}
*/
public submissionId: string; public submissionId: string;
/** /**
@@ -33,6 +60,16 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
*/ */
private subs: Subscription[] = []; private subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} changeDetectorRef
* @param {NotificationsService} notificationsService
* @param {ActivatedRoute} route
* @param {Router} router
* @param {SubmissionService} submissionService
* @param {TranslateService} translate
*/
constructor(private changeDetectorRef: ChangeDetectorRef, constructor(private changeDetectorRef: ChangeDetectorRef,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -41,6 +78,9 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
private translate: TranslateService) { private translate: TranslateService) {
} }
/**
* Retrieve workspaceitem/workflowitem from server and initialize all instance variables
*/
ngOnInit() { ngOnInit() {
this.subs.push(this.route.paramMap.pipe( this.subs.push(this.route.paramMap.pipe(
switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))),
@@ -70,7 +110,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
} }
/** /**
* Method provided by Angular. Invoked when the instance is destroyed. * Unsubscribe from all subscriptions
*/ */
ngOnDestroy() { ngOnDestroy() {
this.subs this.subs

View File

@@ -36,24 +36,48 @@ import { SubmissionService } from '../../submission.service';
import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
/**
* An interface to represent a collection entry
*/
interface CollectionListEntryItem { interface CollectionListEntryItem {
id: string; id: string;
name: string; name: string;
} }
/**
* An interface to represent an entry in the collection list
*/
interface CollectionListEntry { interface CollectionListEntry {
communities: CollectionListEntryItem[], communities: CollectionListEntryItem[],
collection: CollectionListEntryItem collection: CollectionListEntryItem
} }
/**
* This component allows to show the current collection the submission belonging to and to change it.
*/
@Component({ @Component({
selector: 'ds-submission-form-collection', selector: 'ds-submission-form-collection',
styleUrls: ['./submission-form-collection.component.scss'], styleUrls: ['./submission-form-collection.component.scss'],
templateUrl: './submission-form-collection.component.html' templateUrl: './submission-form-collection.component.html'
}) })
export class SubmissionFormCollectionComponent implements OnChanges, OnInit { export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
/**
* The current collection id this submission belonging to
* @type {string}
*/
@Input() currentCollectionId: string; @Input() currentCollectionId: string;
/**
* The current configuration object that define this submission
* @type {SubmissionDefinitionsModel}
*/
@Input() currentDefinition: string; @Input() currentDefinition: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId; @Input() submissionId;
/** /**
@@ -62,18 +86,69 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
*/ */
@Output() collectionChange: EventEmitter<SubmissionObject> = new EventEmitter<SubmissionObject>(); @Output() collectionChange: EventEmitter<SubmissionObject> = new EventEmitter<SubmissionObject>();
/**
* A boolean representing if this dropdown button is disabled
* @type {BehaviorSubject<boolean>}
*/
public disabled$ = new BehaviorSubject<boolean>(true); public disabled$ = new BehaviorSubject<boolean>(true);
public model: any;
/**
* The search form control
* @type {FormControl}
*/
public searchField: FormControl = new FormControl(); public searchField: FormControl = new FormControl();
/**
* The collection list obtained from a search
* @type {Observable<CollectionListEntry[]>}
*/
public searchListCollection$: Observable<CollectionListEntry[]>; public searchListCollection$: Observable<CollectionListEntry[]>;
/**
* The selected collection id
* @type {string}
*/
public selectedCollectionId: string; public selectedCollectionId: string;
/**
* The selected collection name
* @type {Observable<string>}
*/
public selectedCollectionName$: Observable<string>; public selectedCollectionName$: Observable<string>;
/**
* The JsonPatchOperationPathCombiner object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner; protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* A boolean representing if dropdown list is scrollable to the bottom
* @type {boolean}
*/
private scrollableBottom = false; private scrollableBottom = false;
/**
* A boolean representing if dropdown list is scrollable to the top
* @type {boolean}
*/
private scrollableTop = false; private scrollableTop = false;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = []; private subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
* @param {CommunityDataService} communityDataService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {SubmissionService} submissionService
*/
constructor(protected cdr: ChangeDetectorRef, constructor(protected cdr: ChangeDetectorRef,
private communityDataService: CommunityDataService, private communityDataService: CommunityDataService,
private operationsBuilder: JsonPatchOperationsBuilder, private operationsBuilder: JsonPatchOperationsBuilder,
@@ -81,6 +156,13 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
private submissionService: SubmissionService) { private submissionService: SubmissionService) {
} }
/**
* Method called on mousewheel event, it prevent the page scroll
* when arriving at the top/bottom of dropdown menu
*
* @param event
* mousewheel event
*/
@HostListener('mousewheel', ['$event']) onMousewheel(event) { @HostListener('mousewheel', ['$event']) onMousewheel(event) {
if (event.wheelDelta > 0 && this.scrollableTop) { if (event.wheelDelta > 0 && this.scrollableTop) {
event.preventDefault(); event.preventDefault();
@@ -90,11 +172,19 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
} }
} }
/**
* Check if dropdown scrollbar is at the top or bottom of the dropdown list
*
* @param event
*/
onScroll(event) { onScroll(event) {
this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight); this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight);
this.scrollableTop = (event.target.scrollTop === 0); this.scrollableTop = (event.target.scrollTop === 0);
} }
/**
* Initialize collection list
*/
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (hasValue(changes.currentCollectionId) if (hasValue(changes.currentCollectionId)
&& hasValue(changes.currentCollectionId.currentValue)) { && hasValue(changes.currentCollectionId.currentValue)) {
@@ -153,14 +243,26 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
} }
} }
/**
* Initialize all instance variables
*/
ngOnInit() { ngOnInit() {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection'); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
} }
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void { ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
/**
* Emit a [collectionChange] event when a new collection is selected from list
*
* @param event
* the selected [CollectionListEntryItem]
*/
onSelect(event) { onSelect(event) {
this.searchField.reset(); this.searchField.reset();
this.disabled$.next(true); this.disabled$.next(true);
@@ -181,10 +283,19 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
); );
} }
/**
* Reset search form control on dropdown menu close
*/
onClose() { onClose() {
this.searchField.reset(); this.searchField.reset();
} }
/**
* Reset search form control when dropdown menu is closed
*
* @param isOpen
* Representing if the dropdown menu is open or not.
*/
toggled(isOpen: boolean) { toggled(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
this.searchField.reset(); this.searchField.reset();

View File

@@ -13,7 +13,7 @@ import { mockSubmissionId } from '../../../shared/mocks/mock-submission';
import { SubmissionService } from '../../submission.service'; import { SubmissionService } from '../../submission.service';
import { SubmissionRestServiceStub } from '../../../shared/testing/submission-rest-service-stub'; import { SubmissionRestServiceStub } from '../../../shared/testing/submission-rest-service-stub';
import { SubmissionFormFooterComponent } from './submission-form-footer.component'; import { SubmissionFormFooterComponent } from './submission-form-footer.component';
import { SubmissionRestService } from '../../submission-rest.service'; import { SubmissionRestService } from '../../../core/submission/submission-rest.service';
import { createTestComponent } from '../../../shared/testing/utils'; import { createTestComponent } from '../../../shared/testing/utils';
describe('SubmissionFormFooterComponent Component', () => { describe('SubmissionFormFooterComponent Component', () => {

View File

@@ -4,11 +4,14 @@ import { Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { SubmissionRestService } from '../../submission-rest.service'; import { SubmissionRestService } from '../../../core/submission/submission-rest.service';
import { SubmissionService } from '../../submission.service'; import { SubmissionService } from '../../submission.service';
import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; import { SubmissionScopeType } from '../../../core/submission/submission-scope-type';
import { isNotEmpty } from '../../../shared/empty.util'; import { isNotEmpty } from '../../../shared/empty.util';
/**
* This component represents submission form footer bar.
*/
@Component({ @Component({
selector: 'ds-submission-form-footer', selector: 'ds-submission-form-footer',
styleUrls: ['./submission-form-footer.component.scss'], styleUrls: ['./submission-form-footer.component.scss'],
@@ -16,18 +19,51 @@ import { isNotEmpty } from '../../../shared/empty.util';
}) })
export class SubmissionFormFooterComponent implements OnChanges { export class SubmissionFormFooterComponent implements OnChanges {
@Input() submissionId; /**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* A boolean representing if a submission deposit operation is pending
* @type {Observable<boolean>}
*/
public processingDepositStatus: Observable<boolean>; public processingDepositStatus: Observable<boolean>;
/**
* A boolean representing if a submission save operation is pending
* @type {Observable<boolean>}
*/
public processingSaveStatus: Observable<boolean>; public processingSaveStatus: Observable<boolean>;
/**
* A boolean representing if showing deposit and discard buttons
* @type {Observable<boolean>}
*/
public showDepositAndDiscard: Observable<boolean>; public showDepositAndDiscard: Observable<boolean>;
/**
* A boolean representing if submission form is valid or not
* @type {Observable<boolean>}
*/
private submissionIsInvalid: Observable<boolean> = observableOf(true); private submissionIsInvalid: Observable<boolean> = observableOf(true);
/**
* Initialize instance variables
*
* @param {NgbModal} modalService
* @param {SubmissionRestService} restService
* @param {SubmissionService} submissionService
*/
constructor(private modalService: NgbModal, constructor(private modalService: NgbModal,
private restService: SubmissionRestService, private restService: SubmissionRestService,
private submissionService: SubmissionService) { private submissionService: SubmissionService) {
} }
/**
* Initialize all instance variables
*/
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (isNotEmpty(this.submissionId)) { if (isNotEmpty(this.submissionId)) {
this.submissionIsInvalid = this.submissionService.getSubmissionStatus(this.submissionId).pipe( this.submissionIsInvalid = this.submissionService.getSubmissionStatus(this.submissionId).pipe(
@@ -40,18 +76,30 @@ export class SubmissionFormFooterComponent implements OnChanges {
} }
} }
/**
* Dispatch a submission save action
*/
save(event) { save(event) {
this.submissionService.dispatchSave(this.submissionId); this.submissionService.dispatchSave(this.submissionId);
} }
/**
* Dispatch a submission save for later action
*/
saveLater(event) { saveLater(event) {
this.submissionService.dispatchSaveForLater(this.submissionId); this.submissionService.dispatchSaveForLater(this.submissionId);
} }
/**
* Dispatch a submission deposit action
*/
public deposit(event) { public deposit(event) {
this.submissionService.dispatchDeposit(this.submissionId); this.submissionService.dispatchDeposit(this.submissionId);
} }
/**
* Dispatch a submission discard action
*/
public confirmDiscard(content) { public confirmDiscard(content) {
this.modalService.open(content).result.then( this.modalService.open(content).result.then(
(result) => { (result) => {

View File

@@ -1,30 +1,62 @@
import { Component, Input, OnInit, } from '@angular/core'; import { Component, Input, OnInit, } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SectionsService } from '../../sections/sections.service'; import { SectionsService } from '../../sections/sections.service';
import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowService } from '../../../shared/host-window.service';
import { SubmissionService } from '../../submission.service'; import { SubmissionService } from '../../submission.service';
import { SectionDataObject } from '../../sections/models/section-data.model'; import { SectionDataObject } from '../../sections/models/section-data.model';
import { map } from 'rxjs/operators';
/**
* This component allow to add any new section to submission form
*/
@Component({ @Component({
selector: 'ds-submission-form-section-add', selector: 'ds-submission-form-section-add',
styleUrls: [ './submission-form-section-add.component.scss' ], styleUrls: [ './submission-form-section-add.component.scss' ],
templateUrl: './submission-form-section-add.component.html' templateUrl: './submission-form-section-add.component.html'
}) })
export class SubmissionFormSectionAddComponent implements OnInit { export class SubmissionFormSectionAddComponent implements OnInit {
/**
* The collection id this submission belonging to
* @type {string}
*/
@Input() collectionId: string; @Input() collectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string; @Input() submissionId: string;
/**
* The possible section list to add
* @type {Observable<SectionDataObject[]>}
*/
public sectionList$: Observable<SectionDataObject[]>; public sectionList$: Observable<SectionDataObject[]>;
/**
* A boolean representing if there are available sections to add
* @type {Observable<boolean>}
*/
public hasSections$: Observable<boolean>; public hasSections$: Observable<boolean>;
/**
* Initialize instance variables
*
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {HostWindowService} windowService
*/
constructor(private sectionService: SectionsService, constructor(private sectionService: SectionsService,
private submissionService: SubmissionService, private submissionService: SubmissionService,
public windowService: HostWindowService) { public windowService: HostWindowService) {
} }
/**
* Initialize all instance variables
*/
ngOnInit() { ngOnInit() {
this.sectionList$ = this.submissionService.getDisabledSectionsList(this.submissionId); this.sectionList$ = this.submissionService.getDisabledSectionsList(this.submissionId);
this.hasSections$ = this.sectionList$.pipe( this.hasSections$ = this.sectionList$.pipe(
@@ -32,6 +64,9 @@ export class SubmissionFormSectionAddComponent implements OnInit {
) )
} }
/**
* Dispatch an action to add a new section
*/
addSection(sectionId) { addSection(sectionId) {
this.sectionService.addSection(this.submissionId, sectionId); this.sectionService.addSection(this.submissionId, sectionId);
} }

View File

@@ -24,9 +24,9 @@
<div class="submission-form-content"> <div class="submission-form-content">
<ds-loading *ngIf="(isLoading() | async)" message="Loading..."></ds-loading> <ds-loading *ngIf="(isLoading() | async)" message="Loading..."></ds-loading>
<ng-container *ngFor="let object of (submissionSections | async)"> <ng-container *ngFor="let object of (submissionSections | async)">
<ds-submission-form-section-container [collectionId]="collectionId" <ds-submission-section-container [collectionId]="collectionId"
[submissionId]="submissionId" [submissionId]="submissionId"
[sectionData]="object"></ds-submission-form-section-container> [sectionData]="object"></ds-submission-section-container>
</ng-container> </ng-container>
</div> </div>
<div *ngIf="!(isLoading() | async)" class="submission-form-footer mt-3 mb-3 position-sticky"> <div *ngIf="!(isLoading() | async)" class="submission-form-footer mt-3 mb-3 position-sticky">

View File

@@ -63,10 +63,10 @@ describe('SubmissionFormComponent Component', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
const html = ` const html = `
<ds-submission-submit-form [collectionId]="collectionId" <ds-submission-form [collectionId]="collectionId"
[selfUrl]="selfUrl" [selfUrl]="selfUrl"
[submissionDefinition]="submissionDefinition" [submissionDefinition]="submissionDefinition"
[submissionId]="submissionId"></ds-submission-submit-form>`; [submissionId]="submissionId"></ds-submission-form>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance; testComp = testFixture.componentInstance;

View File

@@ -15,22 +15,68 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../core/submission/models/submission-object.model';
/**
* This component represents the submission form.
*/
@Component({ @Component({
selector: 'ds-submission-submit-form', selector: 'ds-submission-form',
styleUrls: ['./submission-form.component.scss'], styleUrls: ['./submission-form.component.scss'],
templateUrl: './submission-form.component.html', templateUrl: './submission-form.component.html',
}) })
export class SubmissionFormComponent implements OnChanges, OnDestroy { export class SubmissionFormComponent implements OnChanges, OnDestroy {
/**
* The collection id this submission belonging to
* @type {string}
*/
@Input() collectionId: string; @Input() collectionId: string;
/**
* The list of submission's sections
* @type {WorkspaceitemSectionsObject}
*/
@Input() sections: WorkspaceitemSectionsObject; @Input() sections: WorkspaceitemSectionsObject;
/**
* The submission self url
* @type {string}
*/
@Input() selfUrl: string; @Input() selfUrl: string;
/**
* The configuration object that define this submission
* @type {SubmissionDefinitionsModel}
*/
@Input() submissionDefinition: SubmissionDefinitionsModel; @Input() submissionDefinition: SubmissionDefinitionsModel;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string; @Input() submissionId: string;
/**
* The configuration id that define this submission
* @type {string}
*/
public definitionId: string; public definitionId: string;
public test = true;
/**
* A boolean representing if a submission form is pending
* @type {Observable<boolean>}
*/
public loading: Observable<boolean> = observableOf(true); public loading: Observable<boolean> = observableOf(true);
public submissionSections: Observable<any>;
/**
* Observable of the list of submission's sections
* @type {Observable<WorkspaceitemSectionsObject>}
*/
public submissionSections: Observable<WorkspaceitemSectionsObject>;
/**
* The uploader configuration options
* @type {UploaderOptions}
*/
public uploadFilesOptions: UploaderOptions = { public uploadFilesOptions: UploaderOptions = {
url: '', url: '',
authToken: null, authToken: null,
@@ -38,9 +84,26 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
itemAlias: null itemAlias: null
}; };
/**
* A boolean representing if component is active
* @type {boolean}
*/
protected isActive: boolean; protected isActive: boolean;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {AuthService} authService
* @param {ChangeDetectorRef} changeDetectorRef
* @param {HALEndpointService} halService
* @param {SubmissionService} submissionService
*/
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@@ -49,9 +112,14 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
this.isActive = true; this.isActive = true;
} }
/**
* Initialize all instance variables and retrieve form configuration
*/
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (this.collectionId && this.submissionId) { if (this.collectionId && this.submissionId) {
this.isActive = true; this.isActive = true;
// retrieve submission's section list
this.submissionSections = this.submissionService.getSubmissionObject(this.submissionId).pipe( this.submissionSections = this.submissionService.getSubmissionObject(this.submissionId).pipe(
filter(() => this.isActive), filter(() => this.isActive),
map((submission: SubmissionObjectEntry) => submission.isLoading), map((submission: SubmissionObjectEntry) => submission.isLoading),
@@ -65,12 +133,14 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
} }
})); }));
// check if is submission loading
this.loading = this.submissionService.getSubmissionObject(this.submissionId).pipe( this.loading = this.submissionService.getSubmissionObject(this.submissionId).pipe(
filter(() => this.isActive), filter(() => this.isActive),
map((submission: SubmissionObjectEntry) => submission.isLoading), map((submission: SubmissionObjectEntry) => submission.isLoading),
map((isLoading: boolean) => isLoading), map((isLoading: boolean) => isLoading),
distinctUntilChanged()); distinctUntilChanged());
// init submission state
this.subs.push( this.subs.push(
this.halService.getEndpoint('workspaceitems').pipe( this.halService.getEndpoint('workspaceitems').pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
@@ -89,10 +159,16 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
}) })
); );
// start auto save
this.submissionService.startAutoSave(this.submissionId); this.submissionService.startAutoSave(this.submissionId);
} }
} }
/**
* Unsubscribe from all subscriptions, destroy instance variables
* and reset submission state
*/
ngOnDestroy() { ngOnDestroy() {
this.isActive = false; this.isActive = false;
this.submissionService.stopAutoSave(); this.submissionService.stopAutoSave();
@@ -102,6 +178,13 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
.forEach((subscription) => subscription.unsubscribe()); .forEach((subscription) => subscription.unsubscribe());
} }
/**
* On collection change reset submission state in case of it has a different
* submission definition
*
* @param submissionObject
* new submission object
*/
onCollectionChange(submissionObject: SubmissionObject) { onCollectionChange(submissionObject: SubmissionObject) {
this.collectionId = (submissionObject.collection as Collection).id; this.collectionId = (submissionObject.collection as Collection).id;
if (this.definitionId !== (submissionObject.submissionDefinition as SubmissionDefinitionsModel).name) { if (this.definitionId !== (submissionObject.submissionDefinition as SubmissionDefinitionsModel).name) {
@@ -119,10 +202,16 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
} }
} }
/**
* Check if submission form is loading
*/
isLoading(): Observable<boolean> { isLoading(): Observable<boolean> {
return this.loading; return this.loading;
} }
/**
* Check if submission form is loading
*/
protected getSectionsList(): Observable<any> { protected getSectionsList(): Observable<any> {
return this.submissionService.getSubmissionSections(this.submissionId).pipe( return this.submissionService.getSubmissionSections(this.submissionId).pipe(
filter((sections: SectionDataObject[]) => isNotEmpty(sections)), filter((sections: SectionDataObject[]) => isNotEmpty(sections)),

View File

@@ -14,24 +14,72 @@ import { UploaderOptions } from '../../../shared/uploader/uploader-options.model
import parseSectionErrors from '../../utils/parseSectionErrors'; import parseSectionErrors from '../../utils/parseSectionErrors';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
/**
* This component represents the drop zone that provides to add files to the submission.
*/
@Component({ @Component({
selector: 'ds-submission-upload-files', selector: 'ds-submission-upload-files',
templateUrl: './submission-upload-files.component.html', templateUrl: './submission-upload-files.component.html',
}) })
export class SubmissionUploadFilesComponent implements OnChanges { export class SubmissionUploadFilesComponent implements OnChanges {
@Input() collectionId; /**
@Input() submissionId; * The collection id this submission belonging to
@Input() sectionId; * @type {string}
*/
@Input() collectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* The upload section id
* @type {string}
*/
@Input() sectionId: string;
/**
* The uploader configuration options
* @type {UploaderOptions}
*/
@Input() uploadFilesOptions: UploaderOptions; @Input() uploadFilesOptions: UploaderOptions;
/**
* A boolean representing if is possible to active drop zone over the document page
* @type {boolean}
*/
public enableDragOverDocument = true; public enableDragOverDocument = true;
/**
* i18n message label
* @type {string}
*/
public dropOverDocumentMsg = 'submission.sections.upload.drop-message'; public dropOverDocumentMsg = 'submission.sections.upload.drop-message';
/**
* i18n message label
* @type {string}
*/
public dropMsg = 'submission.sections.upload.drop-message'; public dropMsg = 'submission.sections.upload.drop-message';
private subs = []; /**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
/**
* A boolean representing if upload functionality is enabled
* @type {boolean}
*/
private uploadEnabled: Observable<boolean> = observableOf(false); private uploadEnabled: Observable<boolean> = observableOf(false);
/**
* Save submission before to upload a file
*/
public onBeforeUpload = () => { public onBeforeUpload = () => {
const sub: Subscription = this.operationsService.jsonPatchByResourceType( const sub: Subscription = this.operationsService.jsonPatchByResourceType(
this.submissionService.getSubmissionObjectLinkName(), this.submissionService.getSubmissionObjectLinkName(),
@@ -42,6 +90,15 @@ export class SubmissionUploadFilesComponent implements OnChanges {
return sub; return sub;
}; };
/**
* Initialize instance variables
*
* @param {NotificationsService} notificationsService
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {TranslateService} translate
*/
constructor(private notificationsService: NotificationsService, constructor(private notificationsService: NotificationsService,
private operationsService: SubmissionJsonPatchOperationsService, private operationsService: SubmissionJsonPatchOperationsService,
private sectionService: SectionsService, private sectionService: SectionsService,
@@ -49,10 +106,19 @@ export class SubmissionUploadFilesComponent implements OnChanges {
private translate: TranslateService) { private translate: TranslateService) {
} }
/**
* Check if upload functionality is enabled
*/
ngOnChanges() { ngOnChanges() {
this.uploadEnabled = this.sectionService.isSectionAvailable(this.submissionId, this.sectionId); this.uploadEnabled = this.sectionService.isSectionAvailable(this.submissionId, this.sectionId);
} }
/**
* Parse the submission object retrieved from REST after upload
*
* @param workspaceitem
* The submission object retrieved from REST
*/
public onCompleteItem(workspaceitem: Workspaceitem) { public onCompleteItem(workspaceitem: Workspaceitem) {
// Checks if upload section is enabled so do upload // Checks if upload section is enabled so do upload
this.subs.push( this.subs.push(
@@ -61,8 +127,8 @@ export class SubmissionUploadFilesComponent implements OnChanges {
.subscribe((isUploadEnabled) => { .subscribe((isUploadEnabled) => {
if (isUploadEnabled) { if (isUploadEnabled) {
const {sections} = workspaceitem; const { sections } = workspaceitem;
const {errors} = workspaceitem; const { errors } = workspaceitem;
const errorsList = parseSectionErrors(errors); const errorsList = parseSectionErrors(errors);
if (sections && isNotEmpty(sections)) { if (sections && isNotEmpty(sections)) {
@@ -87,12 +153,15 @@ export class SubmissionUploadFilesComponent implements OnChanges {
); );
} }
/**
* Show error notification on upload fails
*/
public onUploadError() { public onUploadError() {
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed')); this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
} }
/** /**
* Method provided by Angular. Invoked when the instance is destroyed. * Unsubscribe from all subscriptions
*/ */
ngOnDestroy() { ngOnDestroy() {
this.subs this.subs

View File

@@ -24,7 +24,7 @@ import {
SaveSubmissionFormSuccessAction, SaveSubmissionFormSuccessAction,
SaveSubmissionSectionFormAction, SaveSubmissionSectionFormAction,
SaveSubmissionSectionFormErrorAction, SaveSubmissionSectionFormErrorAction,
SaveSubmissionSectionFormSuccessAction, SaveSubmissionSectionFormSuccessAction, SubmissionObjectAction,
SubmissionObjectActionTypes, SubmissionObjectActionTypes,
UpdateSectionDataAction UpdateSectionDataAction
} from './submission-objects.actions'; } from './submission-objects.actions';
@@ -48,6 +48,9 @@ import { SubmissionJsonPatchOperationsService } from '../../core/submission/subm
@Injectable() @Injectable()
export class SubmissionObjectEffects { export class SubmissionObjectEffects {
/**
* Dispatch a [InitSectionAction] for every submission sections and dispatch a [CompleteInitSubmissionFormAction]
*/
@Effect() loadForm$ = this.actions$.pipe( @Effect() loadForm$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.INIT_SUBMISSION_FORM), ofType(SubmissionObjectActionTypes.INIT_SUBMISSION_FORM),
map((action: InitSubmissionFormAction) => { map((action: InitSubmissionFormAction) => {
@@ -83,6 +86,9 @@ export class SubmissionObjectEffects {
)); ));
})); }));
/**
* Dispatch a [InitSubmissionFormAction]
*/
@Effect() resetForm$ = this.actions$.pipe( @Effect() resetForm$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.RESET_SUBMISSION_FORM), ofType(SubmissionObjectActionTypes.RESET_SUBMISSION_FORM),
map((action: ResetSubmissionFormAction) => map((action: ResetSubmissionFormAction) =>
@@ -95,6 +101,9 @@ export class SubmissionObjectEffects {
null null
))); )));
/**
* Dispatch a [SaveSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error
*/
@Effect() saveSubmission$ = this.actions$.pipe( @Effect() saveSubmission$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM), ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM),
switchMap((action: SaveSubmissionFormAction) => { switchMap((action: SaveSubmissionFormAction) => {
@@ -106,6 +115,9 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
})); }));
/**
* Dispatch a [SaveForLaterSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error
*/
@Effect() saveForLaterSubmission$ = this.actions$.pipe( @Effect() saveForLaterSubmission$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM), ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM),
switchMap((action: SaveForLaterSubmissionFormAction) => { switchMap((action: SaveForLaterSubmissionFormAction) => {
@@ -117,6 +129,9 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
})); }));
/**
* Call parseSaveResponse and dispatch actions
*/
@Effect() saveSubmissionSuccess$ = this.actions$.pipe( @Effect() saveSubmissionSuccess$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS),
withLatestFrom(this.store$), withLatestFrom(this.store$),
@@ -125,6 +140,9 @@ export class SubmissionObjectEffects {
}), }),
mergeMap((actions) => observableFrom(actions))); mergeMap((actions) => observableFrom(actions)));
/**
* Dispatch a [SaveSubmissionSectionFormSuccessAction] or a [SaveSubmissionSectionFormErrorAction] on error
*/
@Effect() saveSection$ = this.actions$.pipe( @Effect() saveSection$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM), ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM),
switchMap((action: SaveSubmissionSectionFormAction) => { switchMap((action: SaveSubmissionSectionFormAction) => {
@@ -137,11 +155,17 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))));
})); }));
/**
* Show a notification on error
*/
@Effect({dispatch: false}) saveError$ = this.actions$.pipe( @Effect({dispatch: false}) saveError$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR), ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR),
withLatestFrom(this.store$), withLatestFrom(this.store$),
tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.save_error_notice')))); tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.save_error_notice'))));
/**
* Call parseSaveResponse and dispatch actions or dispatch [SaveSubmissionFormErrorAction] on error
*/
@Effect() saveAndDeposit$ = this.actions$.pipe( @Effect() saveAndDeposit$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION), ofType(SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION),
withLatestFrom(this.store$), withLatestFrom(this.store$),
@@ -161,6 +185,9 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
})); }));
/**
* Dispatch a [DepositSubmissionSuccessAction] or a [DepositSubmissionErrorAction] on error
*/
@Effect() depositSubmission$ = this.actions$.pipe( @Effect() depositSubmission$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION), ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION),
withLatestFrom(this.store$), withLatestFrom(this.store$),
@@ -170,20 +197,32 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId))));
})); }));
/**
* Show a notification on success and redirect to MyDSpace page
*/
@Effect({dispatch: false}) saveForLaterSubmissionSuccess$ = this.actions$.pipe( @Effect({dispatch: false}) saveForLaterSubmissionSuccess$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS), ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS),
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))),
tap(() => this.submissionService.redirectToMyDSpace())); tap(() => this.submissionService.redirectToMyDSpace()));
/**
* Show a notification on success and redirect to MyDSpace page
*/
@Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$.pipe( @Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS), ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS),
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))),
tap(() => this.submissionService.redirectToMyDSpace())); tap(() => this.submissionService.redirectToMyDSpace()));
/**
* Show a notification on error
*/
@Effect({dispatch: false}) depositSubmissionError$ = this.actions$.pipe( @Effect({dispatch: false}) depositSubmissionError$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR), ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR),
tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.deposit_error_notice')))); tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.deposit_error_notice'))));
/**
* Dispatch a [DiscardSubmissionSuccessAction] or a [DiscardSubmissionErrorAction] on error
*/
@Effect() discardSubmission$ = this.actions$.pipe( @Effect() discardSubmission$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION), ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION),
switchMap((action: DepositSubmissionAction) => { switchMap((action: DepositSubmissionAction) => {
@@ -192,11 +231,17 @@ export class SubmissionObjectEffects {
catchError(() => observableOf(new DiscardSubmissionErrorAction(action.payload.submissionId)))); catchError(() => observableOf(new DiscardSubmissionErrorAction(action.payload.submissionId))));
})); }));
/**
* Show a notification on success and redirect to MyDSpace page
*/
@Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$.pipe( @Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS), ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS),
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))),
tap(() => this.submissionService.redirectToMyDSpace())); tap(() => this.submissionService.redirectToMyDSpace()));
/**
* Show a notification on error
*/
@Effect({dispatch: false}) discardSubmissionError$ = this.actions$.pipe( @Effect({dispatch: false}) discardSubmissionError$ = this.actions$.pipe(
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR), ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR),
tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice')))); tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice'))));
@@ -210,6 +255,12 @@ export class SubmissionObjectEffects {
private translate: TranslateService) { private translate: TranslateService) {
} }
/**
* Check if the submission object retrieved from REST haven't section errors
*
* @param response
* The submission object retrieved from REST
*/
protected canDeposit(response: SubmissionObject[]) { protected canDeposit(response: SubmissionObject[]) {
let canDeposit = true; let canDeposit = true;
@@ -225,7 +276,26 @@ export class SubmissionObjectEffects {
return canDeposit; return canDeposit;
} }
protected parseSaveResponse(currentState: SubmissionObjectEntry, response: SubmissionObject[], submissionId: string, notify: boolean = true) { /**
* Parse the submission object retrieved from REST and return actions to dispatch
*
* @param currentState
* The current SubmissionObjectEntry
* @param response
* The submission object retrieved from REST
* @param submissionId
* The submission id
* @param notify
* A boolean that indicate if show notification or not
* @return SubmissionObjectAction[]
* List of SubmissionObjectAction to dispatch
*/
protected parseSaveResponse(
currentState: SubmissionObjectEntry,
response: SubmissionObject[],
submissionId: string,
notify: boolean = true): SubmissionObjectAction[] {
const mappedActions = []; const mappedActions = [];
if (isNotEmpty(response)) { if (isNotEmpty(response)) {

View File

@@ -38,42 +38,138 @@ import { WorkspaceitemSectionDataType } from '../../core/submission/models/works
import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model';
import { SectionsType } from '../sections/sections-type'; import { SectionsType } from '../sections/sections-type';
/**
* An interface to represent section visibility
*/
export interface SectionVisibility { export interface SectionVisibility {
main: any; main: any;
other: any; other: any;
} }
/**
* An interface to represent section object state
*/
export interface SubmissionSectionObject { export interface SubmissionSectionObject {
/**
* The section header
*/
header: string; header: string;
/**
* The section configuration url
*/
config: string; config: string;
/**
* A boolean representing if this section is mandatory
*/
mandatory: boolean; mandatory: boolean;
/**
* The section type
*/
sectionType: SectionsType; sectionType: SectionsType;
/**
* The section visibility
*/
visibility: SectionVisibility; visibility: SectionVisibility;
/**
* A boolean representing if this section is collapsed
*/
collapsed: boolean, collapsed: boolean,
/**
* A boolean representing if this section is enabled
*/
enabled: boolean; enabled: boolean;
/**
* The section data object
*/
data: WorkspaceitemSectionDataType; data: WorkspaceitemSectionDataType;
/**
* The list of the section errors
*/
errors: SubmissionSectionError[]; errors: SubmissionSectionError[];
/**
* A boolean representing if this section is loading
*/
isLoading: boolean; isLoading: boolean;
/**
* A boolean representing if this section is valid
*/
isValid: boolean; isValid: boolean;
} }
/**
* An interface to represent section error
*/
export interface SubmissionSectionError { export interface SubmissionSectionError {
/**
* A string representing error path
*/
path: string; path: string;
/**
* The error message
*/
message: string; message: string;
} }
/**
* An interface to represent SubmissionSectionObject entry
*/
export interface SubmissionSectionEntry { export interface SubmissionSectionEntry {
[sectionId: string]: SubmissionSectionObject; [sectionId: string]: SubmissionSectionObject;
} }
/**
* An interface to represent submission object state
*/
export interface SubmissionObjectEntry { export interface SubmissionObjectEntry {
/**
* The collection this submission belonging to
*/
collection?: string, collection?: string,
/**
* The configuration name that define this submission
*/
definition?: string, definition?: string,
/**
* The submission self url
*/
selfUrl?: string; selfUrl?: string;
/**
* The submission active section
*/
activeSection?: string; activeSection?: string;
/**
* The list of submission's sections
*/
sections?: SubmissionSectionEntry; sections?: SubmissionSectionEntry;
/**
* A boolean representing if this submission is loading
*/
isLoading?: boolean; isLoading?: boolean;
/**
* A boolean representing if a submission save operation is pending
*/
savePending?: boolean; savePending?: boolean;
/**
* A boolean representing if a submission deposit operation is pending
*/
depositPending?: boolean; depositPending?: boolean;
} }

View File

@@ -7,7 +7,7 @@ import { of as observableOf } from 'rxjs';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SectionContainerComponent } from './section-container.component'; import { SubmissionSectionContainerComponent } from './section-container.component';
import { createTestComponent } from '../../../shared/testing/utils'; import { createTestComponent } from '../../../shared/testing/utils';
import { SectionsType } from '../sections-type'; import { SectionsType } from '../sections-type';
import { SectionsDirective } from '../sections.directive'; import { SectionsDirective } from '../sections.directive';
@@ -41,11 +41,11 @@ const sectionObject: SectionDataObject = {
sectionType: SectionsType.SubmissionForm sectionType: SectionsType.SubmissionForm
}; };
describe('SectionContainerComponent test suite', () => { describe('SubmissionSectionContainerComponent test suite', () => {
let comp: SectionContainerComponent; let comp: SubmissionSectionContainerComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<SectionContainerComponent>; let fixture: ComponentFixture<SubmissionSectionContainerComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let sectionsServiceStub: SectionsServiceStub; let sectionsServiceStub: SectionsServiceStub;
@@ -71,14 +71,14 @@ describe('SectionContainerComponent test suite', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
SectionContainerComponent, SubmissionSectionContainerComponent,
SectionsDirective, SectionsDirective,
TestComponent, TestComponent,
], // declare the test component ], // declare the test component
providers: [ providers: [
{ provide: SectionsService, useClass: SectionsServiceStub }, { provide: SectionsService, useClass: SectionsServiceStub },
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub },
SectionContainerComponent SubmissionSectionContainerComponent
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -93,16 +93,16 @@ describe('SectionContainerComponent test suite', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
html = ` html = `
<ds-submission-form-section-container [collectionId]="collectionId" <ds-submission-section-container [collectionId]="collectionId"
[submissionId]="submissionId" [submissionId]="submissionId"
[sectionData]="object"></ds-submission-form-section-container>`; [sectionData]="object"></ds-submission-section-container>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance; testComp = testFixture.componentInstance;
init(); init();
}); });
it('should create SectionContainerComponent', inject([SectionContainerComponent], (app: SectionContainerComponent) => { it('should create SubmissionSectionContainerComponent', inject([SubmissionSectionContainerComponent], (app: SubmissionSectionContainerComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
})); }));
}); });
@@ -110,7 +110,7 @@ describe('SectionContainerComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
init(); init();
fixture = TestBed.createComponent(SectionContainerComponent); fixture = TestBed.createComponent(SubmissionSectionContainerComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
comp.submissionId = submissionId; comp.submissionId = submissionId;

View File

@@ -3,29 +3,64 @@ import { Component, Injector, Input, OnInit, ViewChild } from '@angular/core';
import { SectionsDirective } from '../sections.directive'; import { SectionsDirective } from '../sections.directive';
import { SectionDataObject } from '../models/section-data.model'; import { SectionDataObject } from '../models/section-data.model';
import { rendersSectionType } from '../sections-decorator'; import { rendersSectionType } from '../sections-decorator';
import { SectionsType } from '../sections-type'; import { AlertType } from '../../../shared/alert/aletr-type';
import { AlertType } from '../../../shared/alerts/aletrs-type';
/**
* This component represents a section that contains the submission license form.
*/
@Component({ @Component({
selector: 'ds-submission-form-section-container', selector: 'ds-submission-section-container',
templateUrl: './section-container.component.html', templateUrl: './section-container.component.html',
styleUrls: ['./section-container.component.scss'] styleUrls: ['./section-container.component.scss']
}) })
export class SectionContainerComponent implements OnInit { export class SubmissionSectionContainerComponent implements OnInit {
/**
* The collection id this submission belonging to
* @type {string}
*/
@Input() collectionId: string; @Input() collectionId: string;
/**
* The section data
* @type {SectionDataObject}
*/
@Input() sectionData: SectionDataObject; @Input() sectionData: SectionDataObject;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string; @Input() submissionId: string;
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType; public AlertTypeEnum = AlertType;
public active = true;
public objectInjector: Injector;
public sectionComponentType: SectionsType;
/**
* Injector to inject a section component with the @Input parameters
* @type {Injector}
*/
public objectInjector: Injector;
/**
* The SectionsDirective reference
*/
@ViewChild('sectionRef') sectionRef: SectionsDirective; @ViewChild('sectionRef') sectionRef: SectionsDirective;
/**
* Initialize instance variables
*
* @param {Injector} injector
*/
constructor(private injector: Injector) { constructor(private injector: Injector) {
} }
/**
* Initialize all instance variables
*/
ngOnInit() { ngOnInit() {
this.objectInjector = Injector.create({ this.objectInjector = Injector.create({
providers: [ providers: [
@@ -37,12 +72,21 @@ export class SectionContainerComponent implements OnInit {
}); });
} }
/**
* Remove section from submission form
*
* @param event
* the event emitted
*/
public removeSection(event) { public removeSection(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.sectionRef.removeSection(this.submissionId, this.sectionData.id); this.sectionRef.removeSection(this.submissionId, this.sectionData.id);
} }
/**
* Find the correct component based on the section's type
*/
getSectionContent(): string { getSectionContent(): string {
return rendersSectionType(this.sectionData.sectionType); return rendersSectionType(this.sectionData.sectionType);
} }

View File

@@ -156,7 +156,7 @@ describe('SectionFormOperationsService test suite', () => {
} }
}; };
expect(service.isPartOfArrayOfGroup(model)).toBeTruthy(); expect(service.isPartOfArrayOfGroup(model as any)).toBeTruthy();
}); });
it('should return false when parent element doesn\'t belong to an array group element', () => { it('should return false when parent element doesn\'t belong to an array group element', () => {
@@ -164,7 +164,7 @@ describe('SectionFormOperationsService test suite', () => {
parent: null parent: null
}; };
expect(service.isPartOfArrayOfGroup(model)).toBeFalsy(); expect(service.isPartOfArrayOfGroup(model as any)).toBeFalsy();
}); });
}); });

View File

@@ -21,12 +21,35 @@ import { FormFieldMetadataValueObject } from '../../../shared/form/builder/model
import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
/**
* The service handling all form section operations
*/
@Injectable() @Injectable()
export class SectionFormOperationsService { export class SectionFormOperationsService {
constructor(private formBuilder: FormBuilderService, private operationsBuilder: JsonPatchOperationsBuilder) { /**
* Initialize service variables
*
* @param {FormBuilderService} formBuilder
* @param {JsonPatchOperationsBuilder} operationsBuilder
*/
constructor(
private formBuilder: FormBuilderService,
private operationsBuilder: JsonPatchOperationsBuilder) {
} }
/**
* Dispatch properly method based on form operation type
*
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
* @param hasStoredValue
* representing if field value related to the specified operation has stored value
*/
public dispatchOperationsFromEvent(pathCombiner: JsonPatchOperationPathCombiner, public dispatchOperationsFromEvent(pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent, event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject, previousValue: FormFieldPreviousValueObject,
@@ -43,6 +66,14 @@ export class SectionFormOperationsService {
} }
} }
/**
* Return index if specified field is part of fields array
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return number
* the array index is part of array, zero otherwise
*/
public getArrayIndexFromEvent(event: DynamicFormControlEvent): number { public getArrayIndexFromEvent(event: DynamicFormControlEvent): number {
let fieldIndex: number; let fieldIndex: number;
if (isNotEmpty(event)) { if (isNotEmpty(event)) {
@@ -60,7 +91,15 @@ export class SectionFormOperationsService {
return isNotUndefined(fieldIndex) ? fieldIndex : 0; return isNotUndefined(fieldIndex) ? fieldIndex : 0;
} }
public isPartOfArrayOfGroup(model: any): boolean { /**
* Check if specified model is part of array of group
*
* @param model
* the [[DynamicFormControlModel]] model
* @return boolean
* true if is part of array, false otherwise
*/
public isPartOfArrayOfGroup(model: DynamicFormControlModel): boolean {
return (isNotNull(model.parent) return (isNotNull(model.parent)
&& (model.parent as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model.parent as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP
&& (model.parent as any).parent && (model.parent as any).parent
@@ -68,7 +107,15 @@ export class SectionFormOperationsService {
&& (model.parent as any).parent.context.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY); && (model.parent as any).parent.context.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY);
} }
public getQualdropValueMap(event): Map<string, any> { /**
* Return a map for the values of a Qualdrop field
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return Map<string, any>
* the map of values
*/
public getQualdropValueMap(event: DynamicFormControlEvent): Map<string, any> {
const metadataValueMap = new Map(); const metadataValueMap = new Map();
const context = this.formBuilder.isQualdropGroup(event.model) const context = this.formBuilder.isQualdropGroup(event.model)
@@ -87,12 +134,28 @@ export class SectionFormOperationsService {
return metadataValueMap; return metadataValueMap;
} }
/**
* Return the absolute path for the field interesting in the specified operation
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return string
* the field path
*/
public getFieldPathFromEvent(event: DynamicFormControlEvent): string { public getFieldPathFromEvent(event: DynamicFormControlEvent): string {
const fieldIndex = this.getArrayIndexFromEvent(event); const fieldIndex = this.getArrayIndexFromEvent(event);
const fieldId = this.getFieldPathSegmentedFromChangeEvent(event); const fieldId = this.getFieldPathSegmentedFromChangeEvent(event);
return (isNotUndefined(fieldIndex)) ? fieldId + '/' + fieldIndex : fieldId; return (isNotUndefined(fieldIndex)) ? fieldId + '/' + fieldIndex : fieldId;
} }
/**
* Return the absolute path for the Qualdrop field interesting in the specified operation
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return string
* the field path
*/
public getQualdropItemPathFromEvent(event: DynamicFormControlEvent): string { public getQualdropItemPathFromEvent(event: DynamicFormControlEvent): string {
const fieldIndex = this.getArrayIndexFromEvent(event); const fieldIndex = this.getArrayIndexFromEvent(event);
const metadataValueMap = new Map(); const metadataValueMap = new Map();
@@ -117,6 +180,14 @@ export class SectionFormOperationsService {
return path; return path;
} }
/**
* Return the segmented path for the field interesting in the specified change operation
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return string
* the field path
*/
public getFieldPathSegmentedFromChangeEvent(event: DynamicFormControlEvent): string { public getFieldPathSegmentedFromChangeEvent(event: DynamicFormControlEvent): string {
let fieldId; let fieldId;
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) { if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
@@ -129,6 +200,14 @@ export class SectionFormOperationsService {
return fieldId; return fieldId;
} }
/**
* Return the value of the field interesting in the specified change operation
*
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @return any
* the field value
*/
public getFieldValueFromChangeEvent(event: DynamicFormControlEvent): any { public getFieldValueFromChangeEvent(event: DynamicFormControlEvent): any {
let fieldValue; let fieldValue;
const value = (event.model as any).value; const value = (event.model as any).value;
@@ -142,12 +221,12 @@ export class SectionFormOperationsService {
if ((event.model as DsDynamicInputModel).hasAuthority) { if ((event.model as DsDynamicInputModel).hasAuthority) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((authority, index) => { value.forEach((authority, index) => {
authority = Object.assign(new AuthorityValue(), authority, {language}); authority = Object.assign(new AuthorityValue(), authority, { language });
value[index] = authority; value[index] = authority;
}); });
fieldValue = value; fieldValue = value;
} else { } else {
fieldValue = Object.assign(new AuthorityValue(), value, {language}); fieldValue = Object.assign(new AuthorityValue(), value, { language });
} }
} else { } else {
// Language without Authority (input, textArea) // Language without Authority (input, textArea)
@@ -162,6 +241,14 @@ export class SectionFormOperationsService {
return fieldValue; return fieldValue;
} }
/**
* Return a map for the values of an array of field
*
* @param items
* the list of items
* @return Map<string, any>
* the map of values
*/
public getValueMap(items: any[]): Map<string, any> { public getValueMap(items: any[]): Map<string, any> {
const metadataValueMap = new Map(); const metadataValueMap = new Map();
@@ -177,6 +264,16 @@ export class SectionFormOperationsService {
return metadataValueMap; return metadataValueMap;
} }
/**
* Handle form remove operations
*
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
*/
protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner, protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent, event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject): void { previousValue: FormFieldPreviousValueObject): void {
@@ -189,6 +286,18 @@ export class SectionFormOperationsService {
} }
} }
/**
* Handle form change operations
*
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
* @param hasStoredValue
* representing if field value related to the specified operation has stored value
*/
protected dispatchOperationsFromChangeEvent(pathCombiner: JsonPatchOperationPathCombiner, protected dispatchOperationsFromChangeEvent(pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent, event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject, previousValue: FormFieldPreviousValueObject,
@@ -243,6 +352,18 @@ export class SectionFormOperationsService {
} }
} }
/**
* Handle form operations interesting a field with a map as value
*
* @param valueMap
* map of values
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
*/
protected dispatchOperationsFromMap(valueMap: Map<string, any>, protected dispatchOperationsFromMap(valueMap: Map<string, any>,
pathCombiner: JsonPatchOperationPathCombiner, pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent, event: DynamicFormControlEvent,

View File

@@ -12,7 +12,7 @@ import { SubmissionServiceStub } from '../../../shared/testing/submission-servic
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service'; import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
import { SectionsService } from '../sections.service'; import { SectionsService } from '../sections.service';
import { SectionsServiceStub } from '../../../shared/testing/sections-service-stub'; import { SectionsServiceStub } from '../../../shared/testing/sections-service-stub';
import { FormSectionComponent } from './section-form.component'; import { SubmissionSectionformComponent } from './section-form.component';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service'; import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
import { getMockFormOperationsService } from '../../../shared/mocks/mock-form-operations-service'; import { getMockFormOperationsService } from '../../../shared/mocks/mock-form-operations-service';
@@ -132,11 +132,11 @@ const dynamicFormControlEvent: DynamicFormControlEvent = {
type: DynamicFormControlEventType.Change type: DynamicFormControlEventType.Change
}; };
describe('FormSectionComponent test suite', () => { describe('SubmissionSectionformComponent test suite', () => {
let comp: FormSectionComponent; let comp: SubmissionSectionformComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<FormSectionComponent>; let fixture: ComponentFixture<SubmissionSectionformComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let sectionsServiceStub: SectionsServiceStub; let sectionsServiceStub: SectionsServiceStub;
let notificationsServiceStub: NotificationsServiceStub; let notificationsServiceStub: NotificationsServiceStub;
@@ -163,7 +163,7 @@ describe('FormSectionComponent test suite', () => {
], ],
declarations: [ declarations: [
FormComponent, FormComponent,
FormSectionComponent, SubmissionSectionformComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -180,7 +180,7 @@ describe('FormSectionComponent test suite', () => {
{ provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'sectionDataProvider', useValue: sectionObject },
{ provide: 'submissionIdProvider', useValue: submissionId }, { provide: 'submissionIdProvider', useValue: submissionId },
ChangeDetectorRef, ChangeDetectorRef,
FormSectionComponent SubmissionSectionformComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -203,7 +203,7 @@ describe('FormSectionComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create FormSectionComponent', inject([FormSectionComponent], (app: FormSectionComponent) => { it('should create SubmissionSectionformComponent', inject([SubmissionSectionformComponent], (app: SubmissionSectionformComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -212,7 +212,7 @@ describe('FormSectionComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FormSectionComponent); fixture = TestBed.createComponent(SubmissionSectionformComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.get(SubmissionService); submissionServiceStub = TestBed.get(SubmissionService);
@@ -236,6 +236,7 @@ describe('FormSectionComponent test suite', () => {
it('should init section properly', () => { it('should init section properly', () => {
const sectionData = {}; const sectionData = {};
formService.isValid.and.returnValue(observableOf(true));
formConfigService.getConfigByHref.and.returnValue(observableOf(formConfigData)); formConfigService.getConfigByHref.and.returnValue(observableOf(formConfigData));
sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionData)); sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionData));
spyOn(comp, 'initForm'); spyOn(comp, 'initForm');
@@ -265,6 +266,7 @@ describe('FormSectionComponent test suite', () => {
it('should set a section Error when init form model fails', () => { it('should set a section Error when init form model fails', () => {
formBuilderService.modelFromConfiguration.and.throwError('test'); formBuilderService.modelFromConfiguration.and.throwError('test');
translateService.instant.and.returnValue('test');
const sectionData = {}; const sectionData = {};
const sectionError: SubmissionSectionError = { const sectionError: SubmissionSectionError = {
message: 'test' + 'Error: test', message: 'test' + 'Error: test',
@@ -317,6 +319,23 @@ describe('FormSectionComponent test suite', () => {
}); });
it('should update form error properly', () => {
spyOn(comp, 'initForm');
spyOn(comp, 'checksForErrors');
const sectionData: any = {
'dc.title': [new FormFieldMetadataValueObject('test')]
};
comp.sectionData.data = {};
comp.sectionData.errors = [];
compAsAny.formData = sectionData;
comp.updateForm(sectionData, parsedSectionErrors);
expect(comp.initForm).not.toHaveBeenCalled();
expect(comp.checksForErrors).toHaveBeenCalled();
expect(comp.sectionData.data).toEqual(sectionData);
});
it('should update form error properly', () => { it('should update form error properly', () => {
spyOn(comp, 'initForm'); spyOn(comp, 'initForm');
spyOn(comp, 'checksForErrors'); spyOn(comp, 'checksForErrors');

View File

@@ -17,7 +17,6 @@ import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';
import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer'; import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer';
import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object';
import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model';
import { GLOBAL_CONFIG } from '../../../../config'; import { GLOBAL_CONFIG } from '../../../../config';
import { GlobalConfig } from '../../../../config/global-config.interface'; import { GlobalConfig } from '../../../../config/global-config.interface';
import { SectionDataObject } from '../models/section-data.model'; import { SectionDataObject } from '../models/section-data.model';
@@ -28,28 +27,95 @@ import { SectionFormOperationsService } from './section-form-operations.service'
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { SectionsService } from '../sections.service'; import { SectionsService } from '../sections.service';
import { difference } from '../../../shared/object.util'; import { difference } from '../../../shared/object.util';
import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model';
/**
* This component represents a section that contains a Form.
*/
@Component({ @Component({
selector: 'ds-submission-section-form', selector: 'ds-submission-section-form',
styleUrls: ['./section-form.component.scss'], styleUrls: ['./section-form.component.scss'],
templateUrl: './section-form.component.html', templateUrl: './section-form.component.html',
}) })
@renderSectionFor(SectionsType.SubmissionForm) @renderSectionFor(SectionsType.SubmissionForm)
export class FormSectionComponent extends SectionModelComponent { export class SubmissionSectionformComponent extends SectionModelComponent {
public formId; /**
* The form id
* @type {string}
*/
public formId: string;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[]; public formModel: DynamicFormControlModel[];
/**
* A boolean representing if this section is updating
* @type {boolean}
*/
public isUpdating = false; public isUpdating = false;
/**
* A boolean representing if this section is loading
* @type {boolean}
*/
public isLoading = true; public isLoading = true;
/**
* The form config
* @type {SubmissionFormsModel}
*/
protected formConfig: SubmissionFormsModel; protected formConfig: SubmissionFormsModel;
/**
* The form data
* @type {any}
*/
protected formData: any = Object.create({}); protected formData: any = Object.create({});
/**
* The [JsonPatchOperationPathCombiner] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner; protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* The [FormFieldPreviousValueObject] object
* @type {FormFieldPreviousValueObject}
*/
protected previousValue: FormFieldPreviousValueObject = new FormFieldPreviousValueObject(); protected previousValue: FormFieldPreviousValueObject = new FormFieldPreviousValueObject();
/**
* The list of Subscription
* @type {Array}
*/
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
/**
* The FormComponent reference
*/
@ViewChild('formRef') private formRef: FormComponent; @ViewChild('formRef') private formRef: FormComponent;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
* @param {FormBuilderService} formBuilderService
* @param {SectionFormOperationsService} formOperationsService
* @param {FormService} formService
* @param {SubmissionFormsConfigService} formConfigService
* @param {NotificationsService} notificationsService
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {TranslateService} translate
* @param {GlobalConfig} EnvConfig
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(protected cdr: ChangeDetectorRef, constructor(protected cdr: ChangeDetectorRef,
protected formBuilderService: FormBuilderService, protected formBuilderService: FormBuilderService,
protected formOperationsService: SectionFormOperationsService, protected formOperationsService: SectionFormOperationsService,
@@ -66,6 +132,9 @@ export class FormSectionComponent extends SectionModelComponent {
super(injectedCollectionId, injectedSectionData, injectedSubmissionId); super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
} }
/**
* Initialize all instance variables and retrieve form configuration
*/
onSectionInit() { onSectionInit() {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
this.formId = this.formService.getUniqueId(this.sectionData.id); this.formId = this.formService.getUniqueId(this.sectionData.id);
@@ -75,30 +144,45 @@ export class FormSectionComponent extends SectionModelComponent {
tap((config: SubmissionFormsModel) => this.formConfig = config), tap((config: SubmissionFormsModel) => this.formConfig = config),
flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)), flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)),
take(1)) take(1))
.subscribe((sectionData: WorkspaceitemSectionDataType) => { .subscribe((sectionData: WorkspaceitemSectionFormObject) => {
if (isUndefined(this.formModel)) { if (isUndefined(this.formModel)) {
this.sectionData.errors = []; this.sectionData.errors = [];
// Is the first loading so init form // Is the first loading so init form
this.initForm(sectionData); this.initForm(sectionData);
this.sectionData.data = sectionData; this.sectionData.data = sectionData;
this.subscriptions(); this.subscriptions();
this.isLoading = false; this.isLoading = false;
this.cdr.detectChanges(); this.cdr.detectChanges();
} }
}) })
} }
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() { onSectionDestroy() {
this.subs this.subs
.filter((subscription) => hasValue(subscription)) .filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe()); .forEach((subscription) => subscription.unsubscribe());
} }
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> { protected getSectionStatus(): Observable<boolean> {
return this.formService.isValid(this.formId); return this.formService.isValid(this.formId);
} }
hasMetadataEnrichment(sectionData): boolean { /**
* Check if the section data has been enriched by the server
*
* @param sectionData
* the section data retrieved from the server
*/
hasMetadataEnrichment(sectionData: WorkspaceitemSectionFormObject): boolean {
const diffResult = []; const diffResult = [];
// compare current form data state with section data retrieved from store // compare current form data state with section data retrieved from store
@@ -116,7 +200,13 @@ export class FormSectionComponent extends SectionModelComponent {
return isNotEmpty(diffResult); return isNotEmpty(diffResult);
} }
initForm(sectionData: WorkspaceitemSectionDataType) { /**
* Initialize form model
*
* @param sectionData
* the section data retrieved from the server
*/
initForm(sectionData: WorkspaceitemSectionFormObject): void {
try { try {
this.formModel = this.formBuilderService.modelFromConfiguration( this.formModel = this.formBuilderService.modelFromConfiguration(
this.formConfig, this.formConfig,
@@ -124,28 +214,32 @@ export class FormSectionComponent extends SectionModelComponent {
sectionData, sectionData,
this.submissionService.getSubmissionScope()); this.submissionService.getSubmissionScope());
} catch (e) { } catch (e) {
this.translate.get('error.submission.sections.init-form-error') const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
.subscribe((msg) => { const sectionError: SubmissionSectionError = {
const sectionError: SubmissionSectionError = { message: msg,
message: msg + e.toString(), path: '/sections/' + this.sectionData.id
path: '/sections/' + this.sectionData.id };
}; this.sectionService.setSectionError(this.submissionId, this.sectionData.id, sectionError);
this.sectionService.setSectionError(this.submissionId, this.sectionData.id, sectionError);
})
} }
} }
updateForm(sectionData: WorkspaceitemSectionDataType, errors: SubmissionSectionError[]) { /**
* Update form model
*
* @param sectionData
* the section data retrieved from the server
* @param errors
* the section errors retrieved from the server
*/
updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void {
if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) { if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) {
this.sectionData.data = sectionData; this.sectionData.data = sectionData;
if (this.hasMetadataEnrichment(sectionData)) { if (this.hasMetadataEnrichment(sectionData)) {
this.translate.get('submission.sections.general.metadata-extracted', { sectionId: this.sectionData.id }) const msg = this.translate.instant(
.pipe(take(1)) 'submission.sections.general.metadata-extracted',
.subscribe((m) => { { sectionId: this.sectionData.id });
this.notificationsService.info(null, m, null, true); this.notificationsService.info(null, msg, null, true);
});
this.isUpdating = true; this.isUpdating = true;
this.formModel = null; this.formModel = null;
this.cdr.detectChanges(); this.cdr.detectChanges();
@@ -153,6 +247,8 @@ export class FormSectionComponent extends SectionModelComponent {
this.checksForErrors(errors); this.checksForErrors(errors);
this.isUpdating = false; this.isUpdating = false;
this.cdr.detectChanges(); this.cdr.detectChanges();
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
this.checksForErrors(errors);
} }
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
this.checksForErrors(errors); this.checksForErrors(errors);
@@ -160,7 +256,13 @@ export class FormSectionComponent extends SectionModelComponent {
} }
checksForErrors(errors: SubmissionSectionError[]) { /**
* Check if there are form validation error retrieved from server
*
* @param errors
* the section errors retrieved from the server
*/
checksForErrors(errors: SubmissionSectionError[]): void {
this.formService.isFormInitialized(this.formId).pipe( this.formService.isFormInitialized(this.formId).pipe(
find((status: boolean) => status === true && !this.isUpdating)) find((status: boolean) => status === true && !this.isUpdating))
.subscribe(() => { .subscribe(() => {
@@ -170,9 +272,11 @@ export class FormSectionComponent extends SectionModelComponent {
}); });
} }
subscriptions() { /**
* Initialize all subscriptions
*/
subscriptions(): void {
this.subs.push( this.subs.push(
/** /**
* Subscribe to form's data * Subscribe to form's data
*/ */
@@ -181,6 +285,7 @@ export class FormSectionComponent extends SectionModelComponent {
.subscribe((formData) => { .subscribe((formData) => {
this.formData = formData; this.formData = formData;
}), }),
/** /**
* Subscribe to section state * Subscribe to section state
*/ */
@@ -190,12 +295,19 @@ export class FormSectionComponent extends SectionModelComponent {
}), }),
distinctUntilChanged()) distinctUntilChanged())
.subscribe((sectionState: SubmissionSectionObject) => { .subscribe((sectionState: SubmissionSectionObject) => {
this.updateForm(sectionState.data, sectionState.errors); this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
}) })
) )
} }
onChange(event: DynamicFormControlEvent) { /**
* Method called when a form dfChange event is fired.
* Dispatch form operations based on changes.
*
* @param event
* the [[DynamicFormControlEvent]] emitted
*/
onChange(event: DynamicFormControlEvent): void {
this.formOperationsService.dispatchOperationsFromEvent( this.formOperationsService.dispatchOperationsFromEvent(
this.pathCombiner, this.pathCombiner,
event, event,
@@ -209,7 +321,14 @@ export class FormSectionComponent extends SectionModelComponent {
} }
} }
onFocus(event: DynamicFormControlEvent) { /**
* Method called when a form dfFocus event is fired.
* Initialize [FormFieldPreviousValueObject] instance.
*
* @param event
* the [[DynamicFormControlEvent]] emitted
*/
onFocus(event: DynamicFormControlEvent): void {
const value = this.formOperationsService.getFieldValueFromChangeEvent(event); const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
const path = this.formBuilderService.getPath(event.model); const path = this.formBuilderService.getPath(event.model);
if (this.formBuilderService.hasMappedGroupValue(event.model)) { if (this.formBuilderService.hasMappedGroupValue(event.model)) {
@@ -221,7 +340,14 @@ export class FormSectionComponent extends SectionModelComponent {
} }
} }
onRemove(event: DynamicFormControlEvent) { /**
* Method called when a form remove event is fired.
* Dispatch form operations based on changes.
*
* @param event
* the [[DynamicFormControlEvent]] emitted
*/
onRemove(event: DynamicFormControlEvent): void {
this.formOperationsService.dispatchOperationsFromEvent( this.formOperationsService.dispatchOperationsFromEvent(
this.pathCombiner, this.pathCombiner,
event, event,
@@ -229,7 +355,15 @@ export class FormSectionComponent extends SectionModelComponent {
this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event)));
} }
hasStoredValue(fieldId, index) { /**
* Check if the specified form field has already a value stored
*
* @param fieldId
* the section data retrieved from the serverù
* @param index
* the section data retrieved from the server
*/
hasStoredValue(fieldId, index): boolean {
if (isNotEmpty(this.sectionData.data)) { if (isNotEmpty(this.sectionData.data)) {
return this.sectionData.data.hasOwnProperty(fieldId) && isNotEmpty(this.sectionData.data[fieldId][index]); return this.sectionData.data.hasOwnProperty(fieldId) && isNotEmpty(this.sectionData.data[fieldId][index]);
} else { } else {

View File

@@ -29,7 +29,7 @@ import {
} from '../../../shared/mocks/mock-submission'; } from '../../../shared/mocks/mock-submission';
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { LicenseSectionComponent } from './section-license.component'; import { SubmissionSectionLicenseComponent } from './section-license.component';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { SectionFormOperationsService } from '../form/section-form-operations.service'; import { SectionFormOperationsService } from '../form/section-form-operations.service';
@@ -78,11 +78,11 @@ const dynamicFormControlEvent: DynamicFormControlEvent = {
type: DynamicFormControlEventType.Change type: DynamicFormControlEventType.Change
}; };
describe('LicenseSectionComponent test suite', () => { describe('SubmissionSectionLicenseComponent test suite', () => {
let comp: LicenseSectionComponent; let comp: SubmissionSectionLicenseComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<LicenseSectionComponent>; let fixture: ComponentFixture<SubmissionSectionLicenseComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let sectionsServiceStub: SectionsServiceStub; let sectionsServiceStub: SectionsServiceStub;
let formService: any; let formService: any;
@@ -124,7 +124,7 @@ describe('LicenseSectionComponent test suite', () => {
], ],
declarations: [ declarations: [
FormComponent, FormComponent,
LicenseSectionComponent, SubmissionSectionLicenseComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -141,7 +141,7 @@ describe('LicenseSectionComponent test suite', () => {
{ provide: 'submissionIdProvider', useValue: submissionId }, { provide: 'submissionIdProvider', useValue: submissionId },
ChangeDetectorRef, ChangeDetectorRef,
FormBuilderService, FormBuilderService,
LicenseSectionComponent SubmissionSectionLicenseComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -164,7 +164,7 @@ describe('LicenseSectionComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create LicenseSectionComponent', inject([LicenseSectionComponent], (app: LicenseSectionComponent) => { it('should create SubmissionSectionLicenseComponent', inject([SubmissionSectionLicenseComponent], (app: SubmissionSectionLicenseComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -173,7 +173,7 @@ describe('LicenseSectionComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LicenseSectionComponent); fixture = TestBed.createComponent(SubmissionSectionLicenseComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.get(SubmissionService); submissionServiceStub = TestBed.get(SubmissionService);

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, find, flatMap, map, startWith, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, find, flatMap, map, startWith, take } from 'rxjs/operators';
import { import {
DynamicCheckboxModel, DynamicCheckboxModel,
DynamicFormControlEvent, DynamicFormControlEvent,
@@ -29,25 +29,79 @@ import { SectionsService } from '../sections.service';
import { SectionFormOperationsService } from '../form/section-form-operations.service'; import { SectionFormOperationsService } from '../form/section-form-operations.service';
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
/**
* This component represents a section that contains the submission license form.
*/
@Component({ @Component({
selector: 'ds-submission-section-license', selector: 'ds-submission-section-license',
styleUrls: ['./section-license.component.scss'], styleUrls: ['./section-license.component.scss'],
templateUrl: './section-license.component.html', templateUrl: './section-license.component.html',
}) })
@renderSectionFor(SectionsType.License) @renderSectionFor(SectionsType.License)
export class LicenseSectionComponent extends SectionModelComponent { export class SubmissionSectionLicenseComponent extends SectionModelComponent {
public formId; /**
* The form id
* @type {string}
*/
public formId: string;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[]; public formModel: DynamicFormControlModel[];
/**
* The [[DynamicFormLayout]] object
* @type {DynamicFormLayout}
*/
public formLayout: DynamicFormLayout = SECTION_LICENSE_FORM_LAYOUT; public formLayout: DynamicFormLayout = SECTION_LICENSE_FORM_LAYOUT;
/**
* A boolean representing if to show form submit and cancel buttons
* @type {boolean}
*/
public displaySubmit = false; public displaySubmit = false;
/**
* The submission license text
* @type {Array}
*/
public licenseText$: Observable<string>; public licenseText$: Observable<string>;
/**
* The [[JsonPatchOperationPathCombiner]] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner; protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
/**
* The FormComponent reference
*/
@ViewChild('formRef') private formRef: FormComponent; @ViewChild('formRef') private formRef: FormComponent;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} changeDetectorRef
* @param {CollectionDataService} collectionDataService
* @param {FormBuilderService} formBuilderService
* @param {SectionFormOperationsService} formOperationsService
* @param {FormService} formService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(protected changeDetectorRef: ChangeDetectorRef, constructor(protected changeDetectorRef: ChangeDetectorRef,
protected collectionDataService: CollectionDataService, protected collectionDataService: CollectionDataService,
protected formBuilderService: FormBuilderService, protected formBuilderService: FormBuilderService,
@@ -62,6 +116,9 @@ export class LicenseSectionComponent extends SectionModelComponent {
super(injectedCollectionId, injectedSectionData, injectedSubmissionId); super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
} }
/**
* Initialize all instance variables and retrieve submission license
*/
onSectionInit() { onSectionInit() {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
this.formId = this.formService.getUniqueId(this.sectionData.id); this.formId = this.formService.getUniqueId(this.sectionData.id);
@@ -126,6 +183,12 @@ export class LicenseSectionComponent extends SectionModelComponent {
); );
} }
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> { protected getSectionStatus(): Observable<boolean> {
const model = this.formBuilderService.findById('granted', this.formModel); const model = this.formBuilderService.findById('granted', this.formModel);
return (model as DynamicCheckboxModel).valueUpdates.pipe( return (model as DynamicCheckboxModel).valueUpdates.pipe(
@@ -133,6 +196,10 @@ export class LicenseSectionComponent extends SectionModelComponent {
startWith((model as DynamicCheckboxModel).value)); startWith((model as DynamicCheckboxModel).value));
} }
/**
* Method called when a form dfChange event is fired.
* Dispatch form operations based on changes.
*/
onChange(event: DynamicFormControlEvent) { onChange(event: DynamicFormControlEvent) {
const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
const value = this.formOperationsService.getFieldValueFromChangeEvent(event); const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
@@ -145,6 +212,9 @@ export class LicenseSectionComponent extends SectionModelComponent {
} }
} }
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() { onSectionDestroy() {
this.subs this.subs
.filter((subscription) => hasValue(subscription)) .filter((subscription) => hasValue(subscription))

View File

@@ -2,14 +2,48 @@ import { SubmissionSectionError } from '../../objects/submission-objects.reducer
import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model';
import { SectionsType } from '../sections-type'; import { SectionsType } from '../sections-type';
/**
* An interface to represent section model
*/
export interface SectionDataObject { export interface SectionDataObject {
/**
* The section configuration url
*/
config: string; config: string;
/**
* The section data object
*/
data: WorkspaceitemSectionDataType; data: WorkspaceitemSectionDataType;
/**
* The list of the section errors
*/
errors: SubmissionSectionError[]; errors: SubmissionSectionError[];
/**
* The section header
*/
header: string; header: string;
/**
* The section id
*/
id: string; id: string;
/**
* A boolean representing if this section is mandatory
*/
mandatory: boolean; mandatory: boolean;
/**
* The section type
*/
sectionType: SectionsType; sectionType: SectionsType;
/**
* Eventually additional fields
*/
[propName: string]: any; [propName: string]: any;
} }

View File

@@ -16,12 +16,44 @@ export interface SectionDataModel {
*/ */
export abstract class SectionModelComponent implements OnDestroy, OnInit, SectionDataModel { export abstract class SectionModelComponent implements OnDestroy, OnInit, SectionDataModel {
protected abstract sectionService: SectionsService; protected abstract sectionService: SectionsService;
/**
* The collection id this submission belonging to
* @type {string}
*/
collectionId: string; collectionId: string;
/**
* The section data
* @type {SectionDataObject}
*/
sectionData: SectionDataObject; sectionData: SectionDataObject;
/**
* The submission id
* @type {string}
*/
submissionId: string; submissionId: string;
/**
* A boolean representing if this section is valid
* @type {boolean}
*/
protected valid: boolean; protected valid: boolean;
/**
* The Subscription to section status observable
* @type {Subscription}
*/
private sectionStatusSub: Subscription; private sectionStatusSub: Subscription;
/**
* Initialize instance variables
*
* @param {string} injectedCollectionId
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
public constructor(@Inject('collectionIdProvider') public injectedCollectionId: string, public constructor(@Inject('collectionIdProvider') public injectedCollectionId: string,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) { @Inject('submissionIdProvider') public injectedSubmissionId: string) {
@@ -30,15 +62,43 @@ export abstract class SectionModelComponent implements OnDestroy, OnInit, Sectio
this.submissionId = injectedSubmissionId; this.submissionId = injectedSubmissionId;
} }
/**
* Call abstract methods on component init
*/
ngOnInit(): void { ngOnInit(): void {
this.onSectionInit(); this.onSectionInit();
this.updateSectionStatus(); this.updateSectionStatus();
} }
/**
* Abstract method to implement to get section status
*
* @return Observable<boolean>
* the section status
*/
protected abstract getSectionStatus(): Observable<boolean>; protected abstract getSectionStatus(): Observable<boolean>;
/**
* Abstract method called on component init.
* It must be used instead of ngOnInit on the component that extend this abstract class
*
* @return Observable<boolean>
* the section status
*/
protected abstract onSectionInit(): void; protected abstract onSectionInit(): void;
/**
* Abstract method called on component destroy.
* It must be used instead of ngOnDestroy on the component that extend this abstract class
*
* @return Observable<boolean>
* the section status
*/
protected abstract onSectionDestroy(): void; protected abstract onSectionDestroy(): void;
/**
* Subscribe to section status
*/
protected updateSectionStatus(): void { protected updateSectionStatus(): void {
this.sectionStatusSub = this.getSectionStatus().pipe( this.sectionStatusSub = this.getSectionStatus().pipe(
filter((sectionStatus: boolean) => isNotUndefined(sectionStatus)), filter((sectionStatus: boolean) => isNotUndefined(sectionStatus)),
@@ -48,6 +108,9 @@ export abstract class SectionModelComponent implements OnDestroy, OnInit, Sectio
}); });
} }
/**
* Unsubscribe from all subscriptions and Call abstract methods on component destroy
*/
ngOnDestroy(): void { ngOnDestroy(): void {
if (hasValue(this.sectionStatusSub)) { if (hasValue(this.sectionStatusSub)) {
this.sectionStatusSub.unsubscribe(); this.sectionStatusSub.unsubscribe();

View File

@@ -10,28 +10,90 @@ import { SubmissionSectionError, SubmissionSectionObject } from '../objects/subm
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
/**
* Directive for handling generic section functionality
*/
@Directive({ @Directive({
selector: '[dsSection]', selector: '[dsSection]',
exportAs: 'sectionRef' exportAs: 'sectionRef'
}) })
export class SectionsDirective implements OnDestroy, OnInit { export class SectionsDirective implements OnDestroy, OnInit {
/**
* A boolean representing if section is mandatory
* @type {boolean}
*/
@Input() mandatory = true; @Input() mandatory = true;
@Input() sectionId;
@Input() submissionId; /**
* The section id
* @type {string}
*/
@Input() sectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* The list of generic errors related to the section
* @type {Array}
*/
public genericSectionErrors: string[] = []; public genericSectionErrors: string[] = [];
/**
* The list of all errors related to the element belonging to this section
* @type {Array}
*/
public allSectionErrors: string[] = []; public allSectionErrors: string[] = [];
/**
* A boolean representing if section is active
* @type {boolean}
*/
private active = true; private active = true;
private animation = !this.mandatory;
/**
* A boolean representing if section is enabled
* @type {boolean}
*/
private enabled: Observable<boolean>; private enabled: Observable<boolean>;
/**
* A boolean representing the panel collapsible state: opened (true) or closed (false)
* @type {boolean}
*/
private sectionState = this.mandatory; private sectionState = this.mandatory;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = []; private subs: Subscription[] = [];
/**
* A boolean representing if section is valid
* @type {boolean}
*/
private valid: Observable<boolean>; private valid: Observable<boolean>;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} changeDetectorRef
* @param {SubmissionService} submissionService
* @param {SectionsService} sectionService
*/
constructor(private changeDetectorRef: ChangeDetectorRef, constructor(private changeDetectorRef: ChangeDetectorRef,
private submissionService: SubmissionService, private submissionService: SubmissionService,
private sectionService: SectionsService) { private sectionService: SectionsService) {
} }
/**
* Initialize instance variables
*/
ngOnInit() { ngOnInit() {
this.valid = this.sectionService.isSectionValid(this.submissionId, this.sectionId).pipe( this.valid = this.sectionService.isSectionValid(this.submissionId, this.sectionId).pipe(
map((valid: boolean) => { map((valid: boolean) => {
@@ -78,67 +140,145 @@ export class SectionsDirective implements OnDestroy, OnInit {
this.enabled = this.sectionService.isSectionEnabled(this.submissionId, this.sectionId); this.enabled = this.sectionService.isSectionEnabled(this.submissionId, this.sectionId);
} }
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy() { ngOnDestroy() {
this.subs this.subs
.filter((subscription) => hasValue(subscription)) .filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe()); .forEach((subscription) => subscription.unsubscribe());
} }
/**
* Change section state
*
* @param event
* the event emitted
*/
public sectionChange(event) { public sectionChange(event) {
this.sectionState = event.nextState; this.sectionState = event.nextState;
} }
public isOpen() { /**
* Check if section panel is open
*
* @returns {boolean}
* Returns true when section panel is open
*/
public isOpen(): boolean {
return this.sectionState; return this.sectionState;
} }
public isMandatory() { /**
* Check if section is mandatory
*
* @returns {boolean}
* Returns true when section is mandatory
*/
public isMandatory(): boolean {
return this.mandatory; return this.mandatory;
} }
public isAnimationsActive() { /**
return this.animation; * Check if section panel is active
} *
* @returns {boolean}
* Returns true when section panel is active
*/
public isSectionActive(): boolean { public isSectionActive(): boolean {
return this.active; return this.active;
} }
/**
* Check if section is enabled
*
* @returns {Observable<boolean>}
* Emits true whenever section is enabled
*/
public isEnabled(): Observable<boolean> { public isEnabled(): Observable<boolean> {
return this.enabled; return this.enabled;
} }
/**
* Check if section is valid
*
* @returns {Observable<boolean>}
* Emits true whenever section is valid
*/
public isValid(): Observable<boolean> { public isValid(): Observable<boolean> {
return this.valid; return this.valid;
} }
public removeSection(submissionId, sectionId) { /**
* Remove section panel from submission form
*
* @param submissionId
* the submission id
* @param sectionId
* the section id
* @returns {Observable<boolean>}
* Emits true whenever section is valid
*/
public removeSection(submissionId: string, sectionId: string) {
this.sectionService.removeSection(submissionId, sectionId) this.sectionService.removeSection(submissionId, sectionId)
} }
public hasGenericErrors() { /**
* Check if section has only generic errors
*
* @returns {boolean}
* Returns true when section has only generic errors
*/
public hasGenericErrors(): boolean {
return this.genericSectionErrors && this.genericSectionErrors.length > 0 return this.genericSectionErrors && this.genericSectionErrors.length > 0
} }
public hasErrors() { /**
* Check if section has errors
*
* @returns {boolean}
* Returns true when section has errors
*/
public hasErrors(): boolean {
return (this.genericSectionErrors && this.genericSectionErrors.length > 0) || return (this.genericSectionErrors && this.genericSectionErrors.length > 0) ||
(this.allSectionErrors && this.allSectionErrors.length > 0) (this.allSectionErrors && this.allSectionErrors.length > 0)
} }
public getErrors() { /**
* Return section errors
*
* @returns {Array}
* Returns section errors list
*/
public getErrors(): string[] {
return this.genericSectionErrors; return this.genericSectionErrors;
} }
public setFocus(event) { /**
* Set form focus to this section panel
*
* @param event
* The event emitted
*/
public setFocus(event): void {
if (!this.active) { if (!this.active) {
this.submissionService.setActiveSection(this.submissionId, this.sectionId); this.submissionService.setActiveSection(this.submissionId, this.sectionId);
} }
} }
public removeError(index) { /**
* Remove error from list
*
* @param index
* The error array key
*/
public removeError(index): void {
this.genericSectionErrors.splice(index); this.genericSectionErrors.splice(index);
} }
/**
* Remove all errors from list
*/
public resetErrors() { public resetErrors() {
if (isNotEmpty(this.genericSectionErrors)) { if (isNotEmpty(this.genericSectionErrors)) {
this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionId); this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionId);

View File

@@ -35,9 +35,20 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
/**
* A service that provides methods used in submission process.
*/
@Injectable() @Injectable()
export class SectionsService { export class SectionsService {
/**
* Initialize service variables
* @param {NotificationsService} notificationsService
* @param {ScrollToService} scrollToService
* @param {SubmissionService} submissionService
* @param {Store<SubmissionState>} store
* @param {TranslateService} translate
*/
constructor(private notificationsService: NotificationsService, constructor(private notificationsService: NotificationsService,
private scrollToService: ScrollToService, private scrollToService: ScrollToService,
private submissionService: SubmissionService, private submissionService: SubmissionService,
@@ -45,17 +56,35 @@ export class SectionsService {
private translate: TranslateService) { private translate: TranslateService) {
} }
/**
* Compare the list of the current section errors with the previous one,
* and dispatch actions to add/remove to/from the section state
*
* @param submissionId
* The submission id
* @param sectionId
* The workspaceitem self url
* @param formId
* The [SubmissionDefinitionsModel] that define submission configuration
* @param currentErrors
* The [SubmissionSectionError] that define submission sections init data
* @param prevErrors
* The [SubmissionSectionError] that define submission sections init errors
*/
public checkSectionErrors( public checkSectionErrors(
submissionId: string, submissionId: string,
sectionId: string, sectionId: string,
formId: string, formId: string,
currentErrors: SubmissionSectionError[], currentErrors: SubmissionSectionError[],
prevErrors: SubmissionSectionError[] = []) { prevErrors: SubmissionSectionError[] = []) {
// Remove previous error list if the current is empty
if (isEmpty(currentErrors)) { if (isEmpty(currentErrors)) {
this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId)); this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
this.store.dispatch(new FormClearErrorsAction(formId)); this.store.dispatch(new FormClearErrorsAction(formId));
} else if (!isEqual(currentErrors, prevErrors)) { } else if (!isEqual(currentErrors, prevErrors)) { // compare previous error list with the current one
const dispatchedErrors = []; const dispatchedErrors = [];
// Itereate over the current error list
currentErrors.forEach((error: SubmissionSectionError) => { currentErrors.forEach((error: SubmissionSectionError) => {
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);
@@ -63,7 +92,7 @@ export class SectionsService {
if (path.fieldId) { if (path.fieldId) {
const fieldId = path.fieldId.replace(/\./g, '_'); const fieldId = path.fieldId.replace(/\./g, '_');
// Dispatch action to the form state; // Dispatch action to add form error to the state;
const formAddErrorAction = new FormAddError(formId, fieldId, path.fieldIndex, error.message); const formAddErrorAction = new FormAddError(formId, fieldId, path.fieldIndex, error.message);
this.store.dispatch(formAddErrorAction); this.store.dispatch(formAddErrorAction);
dispatchedErrors.push(fieldId); dispatchedErrors.push(fieldId);
@@ -71,6 +100,7 @@ export class SectionsService {
}); });
}); });
// Itereate over the previous error list
prevErrors.forEach((error: SubmissionSectionError) => { prevErrors.forEach((error: SubmissionSectionError) => {
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);
@@ -79,6 +109,7 @@ export class SectionsService {
const fieldId = path.fieldId.replace(/\./g, '_'); const fieldId = path.fieldId.replace(/\./g, '_');
if (!dispatchedErrors.includes(fieldId)) { if (!dispatchedErrors.includes(fieldId)) {
// Dispatch action to remove form error from the state;
const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex); const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex);
this.store.dispatch(formRemoveErrorAction); this.store.dispatch(formRemoveErrorAction);
} }
@@ -88,20 +119,58 @@ export class SectionsService {
} }
} }
/**
* Dispatch a new [RemoveSectionErrorsAction]
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
*/
public dispatchRemoveSectionErrors(submissionId, sectionId) { public dispatchRemoveSectionErrors(submissionId, sectionId) {
this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId)); this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
} }
/**
* Return the data object for the specified section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<WorkspaceitemSectionDataType>
* observable of [WorkspaceitemSectionDataType]
*/
public getSectionData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionDataType> { public getSectionData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionDataType> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Return the error list object data for the specified section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<SubmissionSectionError>
* observable of array of [SubmissionSectionError]
*/
public getSectionErrors(submissionId: string, sectionId: string): Observable<SubmissionSectionError[]> { public getSectionErrors(submissionId: string, sectionId: string): Observable<SubmissionSectionError[]> {
return this.store.select(submissionSectionErrorsFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionErrorsFromIdSelector(submissionId, sectionId)).pipe(
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Return the state object for the specified section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<SubmissionSectionObject>
* observable of [SubmissionSectionObject]
*/
public getSectionState(submissionId: string, sectionId: string): Observable<SubmissionSectionObject> { public getSectionState(submissionId: string, sectionId: string): Observable<SubmissionSectionObject> {
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)), filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)),
@@ -109,6 +178,16 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Check if a given section is valid
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<boolean>
* Emits true whenever a given section should be valid
*/
public isSectionValid(submissionId: string, sectionId: string): Observable<boolean> { public isSectionValid(submissionId: string, sectionId: string): Observable<boolean> {
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
filter((sectionObj) => hasValue(sectionObj)), filter((sectionObj) => hasValue(sectionObj)),
@@ -116,12 +195,32 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Check if a given section is active
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<boolean>
* Emits true whenever a given section should be active
*/
public isSectionActive(submissionId: string, sectionId: string): Observable<boolean> { public isSectionActive(submissionId: string, sectionId: string): Observable<boolean> {
return this.submissionService.getActiveSectionId(submissionId).pipe( return this.submissionService.getActiveSectionId(submissionId).pipe(
map((activeSectionId: string) => sectionId === activeSectionId), map((activeSectionId: string) => sectionId === activeSectionId),
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Check if a given section is enabled
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<boolean>
* Emits true whenever a given section should be enabled
*/
public isSectionEnabled(submissionId: string, sectionId: string): Observable<boolean> { public isSectionEnabled(submissionId: string, sectionId: string): Observable<boolean> {
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
filter((sectionObj) => hasValue(sectionObj)), filter((sectionObj) => hasValue(sectionObj)),
@@ -129,6 +228,18 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Check if a given section is a read only section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param submissionScope
* The submission scope
* @return Observable<boolean>
* Emits true whenever a given section should be read only
*/
public isSectionReadOnly(submissionId: string, sectionId: string, submissionScope: SubmissionScopeType): Observable<boolean> { public isSectionReadOnly(submissionId: string, sectionId: string, submissionScope: SubmissionScopeType): Observable<boolean> {
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
filter((sectionObj) => hasValue(sectionObj)), filter((sectionObj) => hasValue(sectionObj)),
@@ -140,6 +251,16 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Check if a given section is a read only available
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<boolean>
* Emits true whenever a given section should be available
*/
public isSectionAvailable(submissionId: string, sectionId: string): Observable<boolean> { public isSectionAvailable(submissionId: string, sectionId: string): Observable<boolean> {
return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)), filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
@@ -149,8 +270,15 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
public addSection(submissionId: string, /**
sectionId: string) { * Dispatch a new [EnableSectionAction] to add a new section and move page target to it
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
*/
public addSection(submissionId: string, sectionId: string) {
this.store.dispatch(new EnableSectionAction(submissionId, sectionId)); this.store.dispatch(new EnableSectionAction(submissionId, sectionId));
const config: ScrollToConfigOptions = { const config: ScrollToConfigOptions = {
target: sectionId, target: sectionId,
@@ -160,11 +288,31 @@ export class SectionsService {
this.scrollToService.scrollTo(config); this.scrollToService.scrollTo(config);
} }
/**
* Dispatch a new [DisableSectionAction] to remove section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
*/
public removeSection(submissionId: string, sectionId: string) { public removeSection(submissionId: string, sectionId: string) {
this.store.dispatch(new DisableSectionAction(submissionId, sectionId)) this.store.dispatch(new DisableSectionAction(submissionId, sectionId))
} }
public updateSectionData(submissionId: string, sectionId: string, data, errors = []) { /**
* Dispatch a new [UpdateSectionDataAction] to update section state with new data and errors
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param data
* The section data
* @param errors
* The list of section errors
*/
public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = []) {
if (isNotEmpty(data)) { if (isNotEmpty(data)) {
const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); const isAvailable$ = this.isSectionAvailable(submissionId, sectionId);
const isEnabled$ = this.isSectionEnabled(submissionId, sectionId); const isEnabled$ = this.isSectionEnabled(submissionId, sectionId);
@@ -182,10 +330,30 @@ export class SectionsService {
} }
} }
/**
* Dispatch a new [InertSectionErrorsAction] to update section state with new error
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param error
* The section error
*/
public setSectionError(submissionId: string, sectionId: string, error: SubmissionSectionError) { public setSectionError(submissionId: string, sectionId: string, error: SubmissionSectionError) {
this.store.dispatch(new InertSectionErrorsAction(submissionId, sectionId, error)); this.store.dispatch(new InertSectionErrorsAction(submissionId, sectionId, error));
} }
/**
* Dispatch a new [SectionStatusChangeAction] to update section state with new status
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param status
* The section status
*/
public setSectionStatus(submissionId: string, sectionId: string, status: boolean) { public setSectionStatus(submissionId: string, sectionId: string, status: boolean) {
this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status)); this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status));
} }

View File

@@ -8,18 +8,37 @@ import { isEmpty } from '../../../../shared/empty.util';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
/**
* This component represents a badge that describe an access condition
*/
@Component({ @Component({
selector: 'ds-access-conditions', selector: 'ds-submission-section-upload-access-conditions',
templateUrl: './accessConditions.component.html', templateUrl: './submission-section-upload-access-conditions.component.html',
}) })
export class AccessConditionsComponent implements OnInit { export class SubmissionSectionUploadAccessConditionsComponent implements OnInit {
/**
* The list of resource policy
* @type {Array}
*/
@Input() accessConditions: ResourcePolicy[]; @Input() accessConditions: ResourcePolicy[];
/**
* The list of access conditions
* @type {Array}
*/
public accessConditionsList = []; public accessConditionsList = [];
/**
* Initialize instance variables
*
* @param {GroupEpersonService} groupService
*/
constructor(private groupService: GroupEpersonService) {} constructor(private groupService: GroupEpersonService) {}
/**
* Retrieve access conditions list
*/
ngOnInit() { ngOnInit() {
this.accessConditions.forEach((accessCondition: ResourcePolicy) => { this.accessConditions.forEach((accessCondition: ResourcePolicy) => {
if (isEmpty(accessCondition.name)) { if (isEmpty(accessCondition.name)) {

View File

@@ -15,7 +15,7 @@ import { createTestComponent } from '../../../../../shared/testing/utils';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import { SubmissionServiceStub } from '../../../../../shared/testing/submission-service-stub'; import { SubmissionServiceStub } from '../../../../../shared/testing/submission-service-stub';
import { SubmissionService } from '../../../../submission.service'; import { SubmissionService } from '../../../../submission.service';
import { UploadSectionFileEditComponent } from './file-edit.component'; import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { import {
mockGroup, mockGroup,
@@ -30,12 +30,13 @@ import { FormService } from '../../../../../shared/form/form.service';
import { GLOBAL_CONFIG } from '../../../../../../config'; import { GLOBAL_CONFIG } from '../../../../../../config';
import { MOCK_SUBMISSION_CONFIG } from '../../../../../shared/testing/mock-submission-config'; import { MOCK_SUBMISSION_CONFIG } from '../../../../../shared/testing/mock-submission-config';
import { getMockFormService } from '../../../../../shared/mocks/mock-form-service'; import { getMockFormService } from '../../../../../shared/mocks/mock-form-service';
import { Group } from '../../../../../core/eperson/models/group.model';
describe('UploadSectionFileEditComponent test suite', () => { describe('SubmissionSectionUploadFileEditComponent test suite', () => {
let comp: UploadSectionFileEditComponent; let comp: SubmissionSectionUploadFileEditComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<UploadSectionFileEditComponent>; let fixture: ComponentFixture<SubmissionSectionUploadFileEditComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let formbuilderService: any; let formbuilderService: any;
@@ -44,7 +45,10 @@ describe('UploadSectionFileEditComponent test suite', () => {
const sectionId = 'upload'; const sectionId = 'upload';
const collectionId = mockSubmissionCollectionId; const collectionId = mockSubmissionCollectionId;
const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions; const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions;
const availableGroupsMap = new Map([[mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid }]]); const availableGroupsMap: Map<string, Group[]> = new Map([
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
const collectionPolicyType = POLICY_DEFAULT_WITH_LIST; const collectionPolicyType = POLICY_DEFAULT_WITH_LIST;
const configMetadataForm: any = mockUploadConfigResponse.metadata; const configMetadataForm: any = mockUploadConfigResponse.metadata;
const fileIndex = '0'; const fileIndex = '0';
@@ -62,7 +66,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
], ],
declarations: [ declarations: [
FormComponent, FormComponent,
UploadSectionFileEditComponent, SubmissionSectionUploadFileEditComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -71,7 +75,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub },
FormBuilderService, FormBuilderService,
ChangeDetectorRef, ChangeDetectorRef,
UploadSectionFileEditComponent SubmissionSectionUploadFileEditComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -84,7 +88,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
const html = ` const html = `
<ds-submission-upload-section-file-edit [availableAccessConditionGroups]="availableAccessConditionGroups" <ds-submission-section-upload-file-edit [availableAccessConditionGroups]="availableAccessConditionGroups"
[availableAccessConditionOptions]="availableAccessConditionOptions" [availableAccessConditionOptions]="availableAccessConditionOptions"
[collectionId]="collectionId" [collectionId]="collectionId"
[collectionPolicyType]="collectionPolicyType" [collectionPolicyType]="collectionPolicyType"
@@ -93,7 +97,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
[fileId]="fileId" [fileId]="fileId"
[fileIndex]="fileIndex" [fileIndex]="fileIndex"
[formId]="formId" [formId]="formId"
[sectionId]="sectionId"></ds-submission-upload-section-file-edit>`; [sectionId]="sectionId"></ds-submission-section-upload-file-edit>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance; testComp = testFixture.componentInstance;
@@ -103,7 +107,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create UploadSectionFileEditComponent', inject([UploadSectionFileEditComponent], (app: UploadSectionFileEditComponent) => { it('should create SubmissionSectionUploadFileEditComponent', inject([SubmissionSectionUploadFileEditComponent], (app: SubmissionSectionUploadFileEditComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -112,7 +116,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UploadSectionFileEditComponent); fixture = TestBed.createComponent(SubmissionSectionUploadFileEditComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.get(SubmissionService); submissionServiceStub = TestBed.get(SubmissionService);

View File

@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnChanges, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { import {
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DynamicDateControlModel, DynamicDateControlModel,
@@ -12,6 +12,8 @@ import {
DynamicFormGroupModel, DynamicFormGroupModel,
DynamicSelectModel DynamicSelectModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import { import {
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
@@ -26,39 +28,122 @@ import {
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT,
BITSTREAM_METADATA_FORM_GROUP_CONFIG, BITSTREAM_METADATA_FORM_GROUP_CONFIG,
BITSTREAM_METADATA_FORM_GROUP_LAYOUT BITSTREAM_METADATA_FORM_GROUP_LAYOUT
} from './files-edit.model'; } from './section-upload-file-edit.model';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { isNotEmpty, isNotUndefined } from '../../../../../shared/empty.util'; import { isNotEmpty, isNotUndefined } from '../../../../../shared/empty.util';
import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model';
import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model'; import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model';
import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model'; import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model';
import { SubmissionService } from '../../../../submission.service'; import { SubmissionService } from '../../../../submission.service';
import { FormService } from '../../../../../shared/form/form.service';
import { FormComponent } from '../../../../../shared/form/form.component';
import { Group } from '../../../../../core/eperson/models/group.model';
/**
* This component represents the edit form for bitstream
*/
@Component({ @Component({
selector: 'ds-submission-upload-section-file-edit', selector: 'ds-submission-section-upload-file-edit',
templateUrl: './file-edit.component.html', templateUrl: './section-upload-file-edit.component.html',
}) })
export class UploadSectionFileEditComponent implements OnChanges { export class SubmissionSectionUploadFileEditComponent implements OnChanges {
/**
* The list of available access condition
* @type {Array}
*/
@Input() availableAccessConditionOptions: any[]; @Input() availableAccessConditionOptions: any[];
@Input() availableAccessConditionGroups: Map<string, any>;
@Input() collectionId;
@Input() collectionPolicyType;
@Input() configMetadataForm: SubmissionFormsModel;
@Input() fileData: WorkspaceitemSectionUploadFileObject;
@Input() fileId;
@Input() fileIndex;
@Input() formId;
@Input() sectionId;
@Input() submissionId;
/**
* The list of available groups for an access condition
* @type {Array}
*/
@Input() availableAccessConditionGroups: Map<string, Group[]>;
/**
* The submission id
* @type {string}
*/
@Input() collectionId: string;
/**
* Define if collection access conditions policy type :
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
* @type {number}
*/
@Input() collectionPolicyType: number;
/**
* The configuration for the bitstream's metadata form
* @type {SubmissionFormsModel}
*/
@Input() configMetadataForm: SubmissionFormsModel;
/**
* The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject}
*/
@Input() fileData: WorkspaceitemSectionUploadFileObject;
/**
* The bitstream id
* @type {string}
*/
@Input() fileId: string;
/**
* The bitstream array key
* @type {string}
*/
@Input() fileIndex: string;
/**
* The form id
* @type {string}
*/
@Input() formId: string;
/**
* The section id
* @type {string}
*/
@Input() sectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[]; public formModel: DynamicFormControlModel[];
/**
* The FormComponent reference
*/
@ViewChild('formRef') public formRef: FormComponent;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
* @param {FormBuilderService} formBuilderService
* @param {FormService} formService
* @param {SubmissionService} submissionService
*/
constructor(private cdr: ChangeDetectorRef, constructor(private cdr: ChangeDetectorRef,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private formService: FormService,
private submissionService: SubmissionService) { private submissionService: SubmissionService) {
} }
/**
* Dispatch form model init
*/
ngOnChanges() { ngOnChanges() {
if (this.fileData && this.formId) { if (this.fileData && this.formId) {
this.formModel = this.buildFileEditForm(); this.formModel = this.buildFileEditForm();
@@ -66,8 +151,10 @@ export class UploadSectionFileEditComponent implements OnChanges {
} }
} }
/**
* Initialize form model
*/
protected buildFileEditForm() { protected buildFileEditForm() {
// TODO check in the rest server configuration whether dc.description may be repeatable
const configDescr: FormFieldModel = Object.assign({}, this.configMetadataForm.rows[0].fields[0]); const configDescr: FormFieldModel = Object.assign({}, this.configMetadataForm.rows[0].fields[0]);
configDescr.repeatable = false; configDescr.repeatable = false;
const configForm = Object.assign({}, this.configMetadataForm, { const configForm = Object.assign({}, this.configMetadataForm, {
@@ -100,7 +187,7 @@ export class UploadSectionFileEditComponent implements OnChanges {
} }
accessConditionTypeModelConfig.options = accessConditionTypeOptions; accessConditionTypeModelConfig.options = accessConditionTypeOptions;
// Dynamic assign of relation in config. For startdate, endDate, groups. // Dynamically assign of relation in config. For startdate, endDate, groups.
const hasStart = []; const hasStart = [];
const hasEnd = []; const hasEnd = [];
const hasGroups = []; const hasGroups = [];
@@ -146,6 +233,12 @@ export class UploadSectionFileEditComponent implements OnChanges {
return formModel; return formModel;
} }
/**
* Initialize form model values
*
* @param formModel
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) { public initModelData(formModel: DynamicFormControlModel[]) {
this.fileData.accessConditions.forEach((accessCondition, index) => { this.fileData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'groupUUID', 'startDate', 'endDate') Array.of('name', 'groupUUID', 'startDate', 'endDate')
@@ -153,8 +246,8 @@ export class UploadSectionFileEditComponent implements OnChanges {
.forEach((key) => { .forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index); const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) { if (metadataModel) {
if (key === 'groupUUID') { if (key === 'groupUUID' && this.availableAccessConditionGroups.get(accessCondition.name)) {
this.availableAccessConditionGroups.forEach((group) => { this.availableAccessConditionGroups.get(accessCondition.name).forEach((group) => {
metadataModel.options.push({ metadataModel.options.push({
label: group.name, label: group.name,
value: group.uuid value: group.uuid
@@ -176,27 +269,47 @@ export class UploadSectionFileEditComponent implements OnChanges {
}); });
} }
/**
* Dispatch form model update when changing an access condition
*
* @param event
* The event emitted
*/
public onChange(event: DynamicFormControlEvent) { public onChange(event: DynamicFormControlEvent) {
if (event.model.id === 'name') { if (event.model.id === 'name') {
this.setOptions(event.model, event.control); this.setOptions(event.model, event.control);
} }
} }
public setOptions(model, control) { /**
* Update `startDate`, 'groupUUID' and 'endDate' model
*
* @param model
* The [[DynamicFormControlModel]] object
* @param control
* The [[FormControl]] object
*/
public setOptions(model: DynamicFormControlModel, control: FormControl) {
let accessCondition: AccessConditionOption = null; let accessCondition: AccessConditionOption = null;
this.availableAccessConditionOptions.filter((element) => element.name === control.value) this.availableAccessConditionOptions.filter((element) => element.name === control.value)
.forEach((element) => accessCondition = element); .forEach((element) => accessCondition = element);
if (isNotEmpty(accessCondition)) { if (isNotEmpty(accessCondition)) {
const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true; const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true;
const groupControl = control.parent.get('groupUUID'); const groupControl: FormControl = control.parent.get('groupUUID') as FormControl;
const startDateControl = control.parent.get('startDate'); const startDateControl: FormControl = control.parent.get('startDate') as FormControl;
const endDateControl = control.parent.get('endDate'); const endDateControl: FormControl = control.parent.get('endDate') as FormControl;
// Clear previous state
groupControl.markAsUntouched();
startDateControl.markAsUntouched();
endDateControl.markAsUntouched();
// Clear previous values // Clear previous values
if (showGroups) { if (showGroups) {
groupControl.setValue(null); groupControl.setValue(null);
} else { } else {
groupControl.clearValidators();
groupControl.setValue(accessCondition.groupUUID); groupControl.setValue(accessCondition.groupUUID);
} }
startDateControl.setValue(null); startDateControl.setValue(null);
@@ -204,15 +317,15 @@ export class UploadSectionFileEditComponent implements OnChanges {
endDateControl.setValue(null); endDateControl.setValue(null);
if (showGroups) { if (showGroups) {
if (isNotUndefined(accessCondition.groupUUID)) { if (isNotUndefined(accessCondition.groupUUID) || isNotUndefined(accessCondition.selectGroupUUID)) {
const groupOptions = []; const groupOptions = [];
if (isNotUndefined(this.availableAccessConditionGroups.get(accessCondition.groupUUID))) { if (isNotUndefined(this.availableAccessConditionGroups.get(accessCondition.name))) {
const groupModel = this.formBuilderService.findById( const groupModel = this.formBuilderService.findById(
'groupUUID', 'groupUUID',
(model.parent as DynamicFormArrayGroupModel).group) as DynamicSelectModel<any>; (model.parent as DynamicFormArrayGroupModel).group) as DynamicSelectModel<any>;
this.availableAccessConditionGroups.forEach((group) => { this.availableAccessConditionGroups.get(accessCondition.name).forEach((group) => {
groupOptions.push({ groupOptions.push({
label: group.name, label: group.name,
value: group.uuid value: group.uuid
@@ -223,8 +336,8 @@ export class UploadSectionFileEditComponent implements OnChanges {
const confGroup = { relation: groupModel.relation }; const confGroup = { relation: groupModel.relation };
const groupsConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, confGroup); const groupsConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, confGroup);
groupsConfig.options = groupOptions; groupsConfig.options = groupOptions;
model.parent.group.pop(); (model.parent as DynamicFormGroupModel).group.pop();
model.parent.group.push(new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT)); (model.parent as DynamicFormGroupModel).group.push(new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT));
} }
} }

View File

@@ -56,7 +56,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke
connective: 'OR', connective: 'OR',
when: [] when: []
} }
] ],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
}
}; };
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = { export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: { element: {
@@ -80,7 +87,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM
connective: 'OR', connective: 'OR',
when: [] when: []
} }
] ],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
}
}; };
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = { export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: { element: {
@@ -102,7 +116,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG: DynamicSelectModelCo
connective: 'OR', connective: 'OR',
when: [] when: []
} }
] ],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.group-required'
}
}; };
export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT: DynamicFormControlLayout = { export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT: DynamicFormControlLayout = {
element: { element: {

View File

@@ -34,9 +34,9 @@
</ng-container> </ng-container>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<ds-submission-upload-section-file-view *ngIf="readMode" <ds-submission-section-upload-file-view *ngIf="readMode"
[fileData]="fileData"></ds-submission-upload-section-file-view> [fileData]="fileData"></ds-submission-section-upload-file-view>
<ds-submission-upload-section-file-edit *ngIf="!readMode" <ds-submission-section-upload-file-edit *ngIf="!readMode"
[availableAccessConditionGroups]="availableAccessConditionGroups" [availableAccessConditionGroups]="availableAccessConditionGroups"
[availableAccessConditionOptions]="availableAccessConditionOptions" [availableAccessConditionOptions]="availableAccessConditionOptions"
[collectionId]="collectionId" [collectionId]="collectionId"
@@ -46,7 +46,7 @@
[fileId]="fileId" [fileId]="fileId"
[fileIndex]="fileIndex" [fileIndex]="fileIndex"
[formId]="formId" [formId]="formId"
[sectionId]="sectionId"></ds-submission-upload-section-file-edit> [sectionId]="sectionId"></ds-submission-section-upload-file-edit>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@@ -15,7 +15,7 @@ import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../../shared/testing/submission-json-patch-operations-service-stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../../shared/testing/submission-json-patch-operations-service-stub';
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
import { UploadSectionFileComponent } from './file.component'; import { SubmissionSectionUploadFileComponent } from './section-upload-file.component';
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service-stub'; import { SubmissionServiceStub } from '../../../../shared/testing/submission-service-stub';
import { import {
mockFileFormData, mockFileFormData,
@@ -35,6 +35,8 @@ import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component';
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { getMockSectionUploadService } from '../../../../shared/mocks/mock-section-upload.service'; import { getMockSectionUploadService } from '../../../../shared/mocks/mock-section-upload.service';
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
import { Group } from '../../../../core/eperson/models/group.model';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
function getMockFileService(): FileService { function getMockFileService(): FileService {
return jasmine.createSpyObj('FileService', { return jasmine.createSpyObj('FileService', {
@@ -43,11 +45,11 @@ function getMockFileService(): FileService {
}); });
} }
describe('UploadSectionFileComponent test suite', () => { describe('SubmissionSectionUploadFileComponent test suite', () => {
let comp: UploadSectionFileComponent; let comp: SubmissionSectionUploadFileComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<UploadSectionFileComponent>; let fixture: ComponentFixture<SubmissionSectionUploadFileComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let uploadService: any; let uploadService: any;
let fileService: any; let fileService: any;
@@ -61,7 +63,10 @@ describe('UploadSectionFileComponent test suite', () => {
const sectionId = 'upload'; const sectionId = 'upload';
const collectionId = mockSubmissionCollectionId; const collectionId = mockSubmissionCollectionId;
const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions; const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions;
const availableGroupsMap = new Map([[mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid }]]); const availableGroupsMap: Map<string, Group[]> = new Map([
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
const collectionPolicyType = POLICY_DEFAULT_WITH_LIST; const collectionPolicyType = POLICY_DEFAULT_WITH_LIST;
const fileIndex = '0'; const fileIndex = '0';
const fileName = '123456-test-upload.jpg'; const fileName = '123456-test-upload.jpg';
@@ -85,7 +90,7 @@ describe('UploadSectionFileComponent test suite', () => {
], ],
declarations: [ declarations: [
FileSizePipe, FileSizePipe,
UploadSectionFileComponent, SubmissionSectionUploadFileComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -98,7 +103,8 @@ describe('UploadSectionFileComponent test suite', () => {
{ provide: SectionUploadService, useValue: getMockSectionUploadService() }, { provide: SectionUploadService, useValue: getMockSectionUploadService() },
ChangeDetectorRef, ChangeDetectorRef,
NgbModal, NgbModal,
UploadSectionFileComponent SubmissionSectionUploadFileComponent,
SubmissionSectionUploadFileEditComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -130,7 +136,7 @@ describe('UploadSectionFileComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create UploadSectionFileComponent', inject([UploadSectionFileComponent], (app: UploadSectionFileComponent) => { it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -139,7 +145,7 @@ describe('UploadSectionFileComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UploadSectionFileComponent); fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.get(SubmissionService); submissionServiceStub = TestBed.get(SubmissionService);
@@ -228,10 +234,14 @@ describe('UploadSectionFileComponent test suite', () => {
expect(fileService.downloadFile).toHaveBeenCalled() expect(fileService.downloadFile).toHaveBeenCalled()
})); }));
it('should download Bitstream File properly', fakeAsync(() => { it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
compAsAny.fileEditComp = TestBed.get(SubmissionSectionUploadFileEditComponent);
compAsAny.fileEditComp.formRef = {formGroup: null};
compAsAny.pathCombiner = pathCombiner; compAsAny.pathCombiner = pathCombiner;
const event = new Event('click', null); const event = new Event('click', null);
spyOn(comp, 'switchMode'); spyOn(comp, 'switchMode');
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockFileFormData)); formService.getFormData.and.returnValue(observableOf(mockFileFormData));
const response = [ const response = [
@@ -279,6 +289,20 @@ describe('UploadSectionFileComponent test suite', () => {
})); }));
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
compAsAny.fileEditComp = TestBed.get(SubmissionSectionUploadFileEditComponent);
compAsAny.fileEditComp.formRef = {formGroup: null};
compAsAny.pathCombiner = pathCombiner;
const event = new Event('click', null);
spyOn(comp, 'switchMode');
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(false));
expect(comp.switchMode).not.toHaveBeenCalled();
expect(uploadService.updateFileData).not.toHaveBeenCalled();
}));
it('should retrieve Value From Field properly', () => { it('should retrieve Value From Field properly', () => {
let field; let field;
expect(compAsAny.retrieveValueFromField(field)).toBeUndefined(); expect(compAsAny.retrieveValueFromField(field)).toBeUndefined();

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { filter, first, flatMap } from 'rxjs/operators'; import { filter, first, flatMap, take } from 'rxjs/operators';
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -20,34 +20,142 @@ import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
import { Group } from '../../../../core/eperson/models/group.model';
/**
* This component represents a single bitstream contained in the submission
*/
@Component({ @Component({
selector: 'ds-submission-upload-section-file', selector: 'ds-submission-upload-section-file',
styleUrls: ['./file.component.scss'], styleUrls: ['./section-upload-file.component.scss'],
templateUrl: './file.component.html', templateUrl: './section-upload-file.component.html',
}) })
export class UploadSectionFileComponent implements OnChanges, OnInit { export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
/**
* The list of available access condition
* @type {Array}
*/
@Input() availableAccessConditionOptions: any[]; @Input() availableAccessConditionOptions: any[];
@Input() availableAccessConditionGroups: Map<string, any>;
@Input() collectionId;
@Input() collectionPolicyType;
@Input() configMetadataForm: SubmissionFormsModel;
@Input() fileId;
@Input() fileIndex;
@Input() fileName;
@Input() sectionId;
@Input() submissionId;
/**
* The list of available groups for an access condition
* @type {Array}
*/
@Input() availableAccessConditionGroups: Map<string, Group[]>;
/**
* The submission id
* @type {string}
*/
@Input() collectionId: string;
/**
* Define if collection access conditions policy type :
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
* @type {number}
*/
@Input() collectionPolicyType: number;
/**
* The configuration for the bitstream's metadata form
* @type {SubmissionFormsModel}
*/
@Input() configMetadataForm: SubmissionFormsModel;
/**
* The bitstream id
* @type {string}
*/
@Input() fileId: string;
/**
* The bitstream array key
* @type {string}
*/
@Input() fileIndex: string;
/**
* The bitstream id
* @type {string}
*/
@Input() fileName: string;
/**
* The section id
* @type {string}
*/
@Input() sectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject}
*/
public fileData: WorkspaceitemSectionUploadFileObject; public fileData: WorkspaceitemSectionUploadFileObject;
public formId;
public readMode; /**
* The form id
* @type {string}
*/
public formId: string;
/**
* A boolean representing if to show bitstream edit form
* @type {boolean}
*/
public readMode: boolean;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[]; public formModel: DynamicFormControlModel[];
/**
* A boolean representing if a submission delete operation is pending
* @type {BehaviorSubject<boolean>}
*/
public processingDelete$ = new BehaviorSubject<boolean>(false); public processingDelete$ = new BehaviorSubject<boolean>(false);
/**
* The [JsonPatchOperationPathCombiner] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner; protected pathCombiner: JsonPatchOperationPathCombiner;
protected subscriptions = [];
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subscriptions: Subscription[] = [];
/**
* The [[SubmissionSectionUploadFileEditComponent]] reference
* @type {SubmissionSectionUploadFileEditComponent}
*/
@ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent;
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
* @param {FileService} fileService
* @param {FormService} formService
* @param {HALEndpointService} halService
* @param {NgbModal} modalService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {SubmissionService} submissionService
* @param {SectionUploadService} uploadService
*/
constructor(private cdr: ChangeDetectorRef, constructor(private cdr: ChangeDetectorRef,
private fileService: FileService, private fileService: FileService,
private formService: FormService, private formService: FormService,
@@ -60,6 +168,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
this.readMode = true; this.readMode = true;
} }
/**
* Retrieve bitstream's metadata
*/
ngOnChanges() { ngOnChanges() {
if (this.availableAccessConditionOptions && this.availableAccessConditionGroups) { if (this.availableAccessConditionOptions && this.availableAccessConditionGroups) {
// Retrieve file state // Retrieve file state
@@ -75,11 +186,17 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
} }
} }
/**
* Initialize instance variables
*/
ngOnInit() { ngOnInit() {
this.formId = this.formService.getUniqueId(this.fileId); this.formId = this.formService.getUniqueId(this.fileId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex);
} }
/**
* Delete bitstream from submission
*/
protected deleteFile() { protected deleteFile() {
this.operationsBuilder.remove(this.pathCombiner.getPath()); this.operationsBuilder.remove(this.pathCombiner.getPath());
this.subscriptions.push(this.operationsService.jsonPatchByResourceID( this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
@@ -93,6 +210,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
})); }));
} }
/**
* Show confirmation dialog for delete
*/
public confirmDelete(content) { public confirmDelete(content) {
this.modalService.open(content).result.then( this.modalService.open(content).result.then(
(result) => { (result) => {
@@ -104,6 +224,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
); );
} }
/**
* Perform bitstream download
*/
public downloadBitstreamFile() { public downloadBitstreamFile() {
this.halService.getEndpoint('bitstreams').pipe( this.halService.getEndpoint('bitstreams').pipe(
first()) first())
@@ -113,12 +236,24 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
}); });
} }
/**
* Save bitstream metadata
*
* @param event
* the click event emitted
*/
public saveBitstreamData(event) { public saveBitstreamData(event) {
event.preventDefault(); event.preventDefault();
this.subscriptions.push(this.formService.getFormData(this.formId).pipe( // validate form
first(), this.formService.validateAllFormFields(this.fileEditComp.formRef.formGroup);
this.subscriptions.push(this.formService.isValid(this.formId).pipe(
take(1),
filter((isValid) => isValid),
flatMap(() => this.formService.getFormData(this.formId)),
take(1),
flatMap((formData: any) => { flatMap((formData: any) => {
// collect bitstream metadata
Object.keys((formData.metadata)) Object.keys((formData.metadata))
.filter((key) => isNotEmpty(formData.metadata[key])) .filter((key) => isNotEmpty(formData.metadata[key]))
.forEach((key) => { .forEach((key) => {
@@ -166,6 +301,7 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
} }
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID( return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(), this.submissionService.getSubmissionObjectLinkName(),
this.submissionId, this.submissionId,
@@ -186,11 +322,20 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
})); }));
} }
private retrieveValueFromField(field) { /**
* Retrieve field value
*
* @param field
* the specified field object
*/
private retrieveValueFromField(field: any) {
const temp = Array.isArray(field) ? field[0] : field; const temp = Array.isArray(field) ? field[0] : field;
return (temp) ? temp.value : undefined; return (temp) ? temp.value : undefined;
} }
/**
* Switch from edit form to metadata view
*/
public switchMode() { public switchMode() {
this.readMode = !this.readMode; this.readMode = !this.readMode;
this.cdr.detectChanges(); this.cdr.detectChanges();

View File

@@ -25,5 +25,5 @@
</ng-container> </ng-container>
<span class="clearfix"></span> <span class="clearfix"></span>
<ds-access-conditions [accessConditions]="fileData.accessConditions"></ds-access-conditions> <ds-submission-section-upload-access-conditions [accessConditions]="fileData.accessConditions"></ds-submission-section-upload-access-conditions>
</div> </div>

View File

@@ -6,15 +6,15 @@ import { TranslateModule } from '@ngx-translate/core';
import { createTestComponent } from '../../../../../shared/testing/utils'; import { createTestComponent } from '../../../../../shared/testing/utils';
import { mockUploadFiles } from '../../../../../shared/mocks/mock-submission'; import { mockUploadFiles } from '../../../../../shared/mocks/mock-submission';
import { FormComponent } from '../../../../../shared/form/form.component'; import { FormComponent } from '../../../../../shared/form/form.component';
import { UploadSectionFileViewComponent } from './file-view.component'; import { SubmissionSectionUploadFileViewComponent } from './section-upload-file-view.component';
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { Metadata } from '../../../../../core/shared/metadata.utils'; import { Metadata } from '../../../../../core/shared/metadata.utils';
describe('UploadSectionFileViewComponent test suite', () => { describe('SubmissionSectionUploadFileViewComponent test suite', () => {
let comp: UploadSectionFileViewComponent; let comp: SubmissionSectionUploadFileViewComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<UploadSectionFileViewComponent>; let fixture: ComponentFixture<SubmissionSectionUploadFileViewComponent>;
const fileData: any = mockUploadFiles[0]; const fileData: any = mockUploadFiles[0];
@@ -26,11 +26,11 @@ describe('UploadSectionFileViewComponent test suite', () => {
declarations: [ declarations: [
TruncatePipe, TruncatePipe,
FormComponent, FormComponent,
UploadSectionFileViewComponent, SubmissionSectionUploadFileViewComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
UploadSectionFileViewComponent SubmissionSectionUploadFileViewComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -43,7 +43,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
const html = ` const html = `
<ds-submission-upload-section-file-view [fileData]="fileData"></ds-submission-upload-section-file-view>`; <ds-submission-section-upload-file-view [fileData]="fileData"></ds-submission-section-upload-file-view>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance; testComp = testFixture.componentInstance;
@@ -53,7 +53,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create UploadSectionFileViewComponent', inject([UploadSectionFileViewComponent], (app: UploadSectionFileViewComponent) => { it('should create SubmissionSectionUploadFileViewComponent', inject([SubmissionSectionUploadFileViewComponent], (app: SubmissionSectionUploadFileViewComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -62,7 +62,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UploadSectionFileViewComponent); fixture = TestBed.createComponent(SubmissionSectionUploadFileViewComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
}); });

View File

@@ -5,17 +5,42 @@ import { isNotEmpty } from '../../../../../shared/empty.util';
import { Metadata } from '../../../../../core/shared/metadata.utils'; import { Metadata } from '../../../../../core/shared/metadata.utils';
import { MetadataMap, MetadataValue } from '../../../../../core/shared/metadata.models'; import { MetadataMap, MetadataValue } from '../../../../../core/shared/metadata.models';
/**
* This component allow to show bitstream's metadata
*/
@Component({ @Component({
selector: 'ds-submission-upload-section-file-view', selector: 'ds-submission-section-upload-file-view',
templateUrl: './file-view.component.html', templateUrl: './section-upload-file-view.component.html',
}) })
export class UploadSectionFileViewComponent implements OnInit { export class SubmissionSectionUploadFileViewComponent implements OnInit {
/**
* The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject}
*/
@Input() fileData: WorkspaceitemSectionUploadFileObject; @Input() fileData: WorkspaceitemSectionUploadFileObject;
/**
* The [[MetadataMap]] object
* @type {MetadataMap}
*/
public metadata: MetadataMap = Object.create({}); public metadata: MetadataMap = Object.create({});
/**
* The bitstream's title key
* @type {string}
*/
public fileTitleKey = 'Title'; public fileTitleKey = 'Title';
/**
* The bitstream's description key
* @type {string}
*/
public fileDescrKey = 'Description'; public fileDescrKey = 'Description';
/**
* Initialize instance variables
*/
ngOnInit() { ngOnInit() {
if (isNotEmpty(this.fileData.metadata)) { if (isNotEmpty(this.fileData.metadata)) {
this.metadata[this.fileTitleKey] = Metadata.all(this.fileData.metadata, 'dc.title'); this.metadata[this.fileTitleKey] = Metadata.all(this.fileData.metadata, 'dc.title');
@@ -23,7 +48,15 @@ export class UploadSectionFileViewComponent implements OnInit {
} }
} }
getAllMetadataValue(metadataKey): MetadataValue[] { /**
* Gets all matching metadata in the map(s)
*
* @param metadataKey
* The metadata key(s) in scope
* @returns {MetadataValue[]}
* The matching values
*/
getAllMetadataValue(metadataKey: string): MetadataValue[] {
return Metadata.all(this.metadata, metadataKey); return Metadata.all(this.metadata, metadataKey);
} }
} }

View File

@@ -15,17 +15,14 @@
<div *ngIf="collectionDefaultAccessConditions.length > 0" class="row"> <div *ngIf="collectionDefaultAccessConditions.length > 0" class="row">
<div class="col-sm-12" > <div class="col-sm-12" >
<ds-alert [type]="AlertTypeEnum.Warning"> <ds-alert [type]="AlertTypeEnum.Warning">
<!-- no def , no banner -->
<ng-container *ngIf="collectionPolicyType === 1"> <ng-container *ngIf="collectionPolicyType === 1">
<!-- def e no scelta -->
{{ 'submission.sections.upload.header.policy.default.nolist' | translate:{ "collectionName": collectionName } }} {{ 'submission.sections.upload.header.policy.default.nolist' | translate:{ "collectionName": collectionName } }}
</ng-container> </ng-container>
<ng-container *ngIf="collectionPolicyType === 2"> <ng-container *ngIf="collectionPolicyType === 2">
<!-- def e scelta -->
{{ 'submission.sections.upload.header.policy.default.withlist' | translate:{ "collectionName": collectionName } }} {{ 'submission.sections.upload.header.policy.default.withlist' | translate:{ "collectionName": collectionName } }}
</ng-container> </ng-container>
<span class="clearfix"></span> <span class="clearfix"></span>
<ds-access-conditions [accessConditions]="collectionDefaultAccessConditions"></ds-access-conditions> <ds-submission-section-upload-access-conditions [accessConditions]="collectionDefaultAccessConditions"></ds-submission-section-upload-access-conditions>
</ds-alert> </ds-alert>
</div> </div>
</div> </div>

View File

@@ -24,7 +24,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service';
import { SectionUploadService } from './section-upload.service'; import { SectionUploadService } from './section-upload.service';
import { UploadSectionComponent } from './section-upload.component'; import { SubmissionSectionUploadComponent } from './section-upload.component';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; import { GroupEpersonService } from '../../../core/eperson/group-eperson.service';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
@@ -71,11 +71,11 @@ const sectionObject: SectionDataObject = {
sectionType: SectionsType.Upload sectionType: SectionsType.Upload
}; };
describe('UploadSectionComponent test suite', () => { describe('SubmissionSectionUploadComponent test suite', () => {
let comp: UploadSectionComponent; let comp: SubmissionSectionUploadComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<UploadSectionComponent>; let fixture: ComponentFixture<SubmissionSectionUploadComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let sectionsServiceStub: SectionsServiceStub; let sectionsServiceStub: SectionsServiceStub;
let collectionDataService: any; let collectionDataService: any;
@@ -114,7 +114,7 @@ describe('UploadSectionComponent test suite', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
UploadSectionComponent, SubmissionSectionUploadComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -127,7 +127,7 @@ describe('UploadSectionComponent test suite', () => {
{ provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'sectionDataProvider', useValue: sectionObject },
{ provide: 'submissionIdProvider', useValue: submissionId }, { provide: 'submissionIdProvider', useValue: submissionId },
ChangeDetectorRef, ChangeDetectorRef,
UploadSectionComponent SubmissionSectionUploadComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -150,7 +150,7 @@ describe('UploadSectionComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create UploadSectionComponent', inject([UploadSectionComponent], (app: UploadSectionComponent) => { it('should create SubmissionSectionUploadComponent', inject([SubmissionSectionUploadComponent], (app: SubmissionSectionUploadComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -159,7 +159,7 @@ describe('UploadSectionComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UploadSectionComponent); fixture = TestBed.createComponent(SubmissionSectionUploadComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.get(SubmissionService); submissionServiceStub = TestBed.get(SubmissionService);
@@ -204,15 +204,17 @@ describe('UploadSectionComponent test suite', () => {
comp.onSectionInit(); comp.onSectionInit();
const expectedGroupsMap = new Map(); const expectedGroupsMap = new Map([
expectedGroupsMap.set(mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid }); [mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
expect(comp.collectionId).toBe(collectionId); expect(comp.collectionId).toBe(collectionId);
expect(comp.collectionName).toBe(mockCollection.name); expect(comp.collectionName).toBe(mockCollection.name);
expect(comp.availableAccessConditionOptions.length).toBe(4); expect(comp.availableAccessConditionOptions.length).toBe(4);
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any); expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
expect(compAsAny.subs.length).toBe(2); expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.availableGroups.size).toBe(1); expect(compAsAny.availableGroups.size).toBe(2);
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap); expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
expect(compAsAny.fileList).toEqual([]); expect(compAsAny.fileList).toEqual([]);
expect(compAsAny.fileIndexes).toEqual([]); expect(compAsAny.fileIndexes).toEqual([]);
@@ -248,15 +250,17 @@ describe('UploadSectionComponent test suite', () => {
comp.onSectionInit(); comp.onSectionInit();
const expectedGroupsMap = new Map(); const expectedGroupsMap = new Map([
expectedGroupsMap.set(mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid }); [mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
expect(comp.collectionId).toBe(collectionId); expect(comp.collectionId).toBe(collectionId);
expect(comp.collectionName).toBe(mockCollection.name); expect(comp.collectionName).toBe(mockCollection.name);
expect(comp.availableAccessConditionOptions.length).toBe(4); expect(comp.availableAccessConditionOptions.length).toBe(4);
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any); expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
expect(compAsAny.subs.length).toBe(2); expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.availableGroups.size).toBe(1); expect(compAsAny.availableGroups.size).toBe(2);
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap); expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
expect(compAsAny.fileList).toEqual(mockUploadFiles); expect(compAsAny.fileList).toEqual(mockUploadFiles);
expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']); expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']);

View File

@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators';
import { SectionModelComponent } from '../models/section.model'; import { SectionModelComponent } from '../models/section.model';
@@ -15,7 +15,7 @@ import { SectionsType } from '../sections-type';
import { renderSectionFor } from '../sections-decorator'; import { renderSectionFor } from '../sections-decorator';
import { SectionDataObject } from '../models/section-data.model'; import { SectionDataObject } from '../models/section-data.model';
import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer'; import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer';
import { AlertType } from '../../../shared/alerts/aletrs-type'; import { AlertType } from '../../../shared/alert/aletr-type';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { SectionsService } from '../sections.service'; import { SectionsService } from '../sections.service';
@@ -23,49 +23,105 @@ import { SubmissionService } from '../../submission.service';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; import { ResourcePolicy } from '../../../core/shared/resource-policy.model';
import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model';
import { PaginatedList } from '../../../core/data/paginated-list';
export const POLICY_DEFAULT_NO_LIST = 1; // Banner1 export const POLICY_DEFAULT_NO_LIST = 1; // Banner1
export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2 export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2
export interface AccessConditionGroupsMapEntry {
accessCondition: string;
groups: Group[]
}
/**
* This component represents a section that contains submission's bitstreams
*/
@Component({ @Component({
selector: 'ds-submission-section-upload', selector: 'ds-submission-section-upload',
styleUrls: ['./section-upload.component.scss'], styleUrls: ['./section-upload.component.scss'],
templateUrl: './section-upload.component.html', templateUrl: './section-upload.component.html',
}) })
@renderSectionFor(SectionsType.Upload) @renderSectionFor(SectionsType.Upload)
export class UploadSectionComponent extends SectionModelComponent { export class SubmissionSectionUploadComponent extends SectionModelComponent {
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType; public AlertTypeEnum = AlertType;
public fileIndexes = [];
public fileList = [];
public fileNames = [];
/**
* The array containing the keys of file list array
* @type {Array}
*/
public fileIndexes: string[] = [];
/**
* The file list
* @type {Array}
*/
public fileList: any[] = [];
/**
* The array containing the name of the files
* @type {Array}
*/
public fileNames: string[] = [];
/**
* The collection name this submission belonging to
* @type {string}
*/
public collectionName: string; public collectionName: string;
/* /**
* Default access conditions of this collection * Default access conditions of this collection
* @type {Array}
*/ */
public collectionDefaultAccessConditions: any[] = []; public collectionDefaultAccessConditions: any[] = [];
/* /**
* The collection access conditions policy * Define if collection access conditions policy type :
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
* @type {number}
*/ */
public collectionPolicyType; public collectionPolicyType: number;
/**
* The configuration for the bitstream's metadata form
*/
public configMetadataForm$: Observable<SubmissionFormsModel>; public configMetadataForm$: Observable<SubmissionFormsModel>;
/* /**
* List of available access conditions that could be setted to files * List of available access conditions that could be setted to files
*/ */
public availableAccessConditionOptions: AccessConditionOption[]; // List of accessConditions that an user can select public availableAccessConditionOptions: AccessConditionOption[]; // List of accessConditions that an user can select
/* /**
* List of Groups available for every access condition * List of Groups available for every access condition
*/ */
protected availableGroups: Map<string, any>; // Groups for any policy protected availableGroups: Map<string, Group[]>; // Groups for any policy
protected subs = []; /**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {SectionUploadService} bitstreamService
* @param {ChangeDetectorRef} changeDetectorRef
* @param {CollectionDataService} collectionDataService
* @param {GroupEpersonService} groupService
* @param {SectionsService} sectionService
* @param {SubmissionService} submissionService
* @param {SubmissionUploadsConfigService} uploadsConfigService
* @param {SectionDataObject} injectedSectionData
* @param {string} injectedSubmissionId
*/
constructor(private bitstreamService: SectionUploadService, constructor(private bitstreamService: SectionUploadService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private collectionDataService: CollectionDataService, private collectionDataService: CollectionDataService,
@@ -78,10 +134,14 @@ export class UploadSectionComponent extends SectionModelComponent {
super(undefined, injectedSectionData, injectedSubmissionId); super(undefined, injectedSectionData, injectedSubmissionId);
} }
/**
* Initialize all instance variables and retrieve collection default access conditions
*/
onSectionInit() { onSectionInit() {
const config$ = this.uploadsConfigService.getConfigByHref(this.sectionData.config).pipe( const config$ = this.uploadsConfigService.getConfigByHref(this.sectionData.config).pipe(
map((config) => config.payload)); map((config) => config.payload));
// retrieve configuration for the bitstream's metadata form
this.configMetadataForm$ = config$.pipe( this.configMetadataForm$ = config$.pipe(
take(1), take(1),
map((config: SubmissionUploadsModel) => config.metadata)); map((config: SubmissionUploadsModel) => config.metadata));
@@ -117,41 +177,48 @@ export class UploadSectionComponent extends SectionModelComponent {
: POLICY_DEFAULT_NO_LIST; : POLICY_DEFAULT_NO_LIST;
this.availableGroups = new Map(); this.availableGroups = new Map();
const groups$ = []; const mapGroups$: Array<Observable<AccessConditionGroupsMapEntry>> = [];
// Retrieve Groups for accessConditionPolicies // Retrieve Groups for accessCondition Policies
this.availableAccessConditionOptions.forEach((accessCondition: AccessConditionOption) => { this.availableAccessConditionOptions.forEach((accessCondition: AccessConditionOption) => {
if (accessCondition.hasEndDate === true || accessCondition.hasStartDate === true) { if (accessCondition.hasEndDate === true || accessCondition.hasStartDate === true) {
groups$.push( if (accessCondition.groupUUID) {
this.groupService.findById(accessCondition.groupUUID).pipe( mapGroups$.push(
find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded)) this.groupService.findById(accessCondition.groupUUID).pipe(
); find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded),
map((rd: RemoteData<Group>) => ({
accessCondition: accessCondition.name,
groups: [rd.payload]
} as AccessConditionGroupsMapEntry)))
);
} else if (accessCondition.selectGroupUUID) {
mapGroups$.push(
this.groupService.findById(accessCondition.selectGroupUUID).pipe(
find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded),
flatMap((group: RemoteData<Group>) => group.payload.groups),
find((rd: RemoteData<PaginatedList<Group>>) => !rd.isResponsePending && rd.hasSucceeded),
map((rd: RemoteData<PaginatedList<Group>>) => ({
accessCondition: accessCondition.name,
groups: rd.payload.page
} as AccessConditionGroupsMapEntry))
));
}
} }
}); });
return groups$; return mapGroups$;
}), }),
flatMap((group) => group), flatMap((entry) => entry),
reduce((acc: Group[], group: RemoteData<Group>) => { reduce((acc: any[], entry: AccessConditionGroupsMapEntry) => {
acc.push(group.payload); acc.push(entry);
return acc; return acc;
}, []), }, []),
).subscribe((groups: Group[]) => { ).subscribe((entries: AccessConditionGroupsMapEntry[]) => {
groups.forEach((group: Group) => { entries.forEach((entry: AccessConditionGroupsMapEntry) => {
if (isUndefined(this.availableGroups.get(group.uuid))) { this.availableGroups.set(entry.accessCondition, entry.groups);
if (Array.isArray(group.groups)) {
const groupArrayData = [];
for (const groupData of group.groups) {
groupArrayData.push({ name: groupData.name, uuid: groupData.uuid });
}
this.availableGroups.set(group.uuid, groupArrayData);
} else {
this.availableGroups.set(group.uuid, { name: group.name, uuid: group.uuid });
}
}
}); });
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
}) }),
,
// retrieve submission's bitstreams from state
combineLatest(this.configMetadataForm$, combineLatest(this.configMetadataForm$,
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe( this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => { filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
@@ -159,24 +226,32 @@ export class UploadSectionComponent extends SectionModelComponent {
}), }),
distinctUntilChanged()) distinctUntilChanged())
.subscribe(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => { .subscribe(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
this.fileList = []; this.fileList = [];
this.fileIndexes = []; this.fileIndexes = [];
this.fileNames = []; this.fileNames = [];
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
if (isNotUndefined(fileList) && fileList.length > 0) { if (isNotUndefined(fileList) && fileList.length > 0) {
fileList.forEach((file) => { fileList.forEach((file) => {
this.fileList.push(file); this.fileList.push(file);
this.fileIndexes.push(file.uuid); this.fileIndexes.push(file.uuid);
this.fileNames.push(this.getFileName(configMetadataForm, file)); this.fileNames.push(this.getFileName(configMetadataForm, file));
}); });
} }
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
} }
) )
); );
} }
/**
* Return file name from metadata
*
* @param configMetadataForm
* the bitstream's form configuration
* @param fileData
* the file metadata
*/
private getFileName(configMetadataForm: SubmissionFormsModel, fileData: any): string { private getFileName(configMetadataForm: SubmissionFormsModel, fileData: any): string {
const metadataName: string = configMetadataForm.rows[0].fields[0].selectableMetadata[0].metadata; const metadataName: string = configMetadataForm.rows[0].fields[0].selectableMetadata[0].metadata;
let title: string; let title: string;
@@ -189,6 +264,12 @@ export class UploadSectionComponent extends SectionModelComponent {
return title; return title;
} }
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> { protected getSectionStatus(): Observable<boolean> {
return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe( return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe(
map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0))); map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0)));

View File

@@ -14,51 +14,127 @@ import { submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSe
import { isUndefined } from '../../../shared/empty.util'; import { isUndefined } from '../../../shared/empty.util';
import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model'; import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model';
/**
* A service that provides methods to handle submission's bitstream state.
*/
@Injectable() @Injectable()
export class SectionUploadService { export class SectionUploadService {
/**
* Initialize service variables
*
* @param {Store<SubmissionState>} store
*/
constructor(private store: Store<SubmissionState>) {} constructor(private store: Store<SubmissionState>) {}
/**
* Return submission's bitstream list from state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @returns {Array}
* Returns submission's bitstream list
*/
public getUploadedFileList(submissionId: string, sectionId: string): Observable<any> { public getUploadedFileList(submissionId: string, sectionId: string): Observable<any> {
return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe(
map((state) => state), map((state) => state),
distinctUntilChanged()); distinctUntilChanged());
} }
public getFileData(submissionId: string, sectionId: string, fileUuid: string): Observable<any> { /**
* Return bitstream's metadata
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
* @returns {Observable}
* Emits bitstream's metadata
*/
public getFileData(submissionId: string, sectionId: string, fileUUID: string): Observable<any> {
return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe( return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe(
filter((state) => !isUndefined(state)), filter((state) => !isUndefined(state)),
map((state) => { map((state) => {
let fileState; let fileState;
Object.keys(state) Object.keys(state)
.filter((key) => state[key].uuid === fileUuid) .filter((key) => state[key].uuid === fileUUID)
.forEach((key) => fileState = state[key]); .forEach((key) => fileState = state[key]);
return fileState; return fileState;
}), }),
distinctUntilChanged()); distinctUntilChanged());
} }
public getDefaultPolicies(submissionId: string, sectionId: string, fileId: string): Observable<any> { /**
return this.store.select(submissionUploadedFileFromUuidSelector(submissionId, sectionId, fileId)).pipe( * Return bitstream's default policies
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
* @returns {Observable}
* Emits bitstream's default policies
*/
public getDefaultPolicies(submissionId: string, sectionId: string, fileUUID: string): Observable<any> {
return this.store.select(submissionUploadedFileFromUuidSelector(submissionId, sectionId, fileUUID)).pipe(
map((state) => state), map((state) => state),
distinctUntilChanged()); distinctUntilChanged());
} }
public addUploadedFile(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { /**
* Add a new bitstream to the state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
* @param data
* The [[WorkspaceitemSectionUploadFileObject]] object
*/
public addUploadedFile(submissionId: string, sectionId: string, fileUUID: string, data: WorkspaceitemSectionUploadFileObject) {
this.store.dispatch( this.store.dispatch(
new NewUploadedFileAction(submissionId, sectionId, fileId, data) new NewUploadedFileAction(submissionId, sectionId, fileUUID, data)
); );
} }
public updateFileData(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { /**
* Update bitstream metadata into the state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
* @param data
* The [[WorkspaceitemSectionUploadFileObject]] object
*/
public updateFileData(submissionId: string, sectionId: string, fileUUID: string, data: WorkspaceitemSectionUploadFileObject) {
this.store.dispatch( this.store.dispatch(
new EditFileDataAction(submissionId, sectionId, fileId, data) new EditFileDataAction(submissionId, sectionId, fileUUID, data)
); );
} }
public removeUploadedFile(submissionId: string, sectionId: string, fileId: string) { /**
* Remove bitstream from the state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
*/
public removeUploadedFile(submissionId: string, sectionId: string, fileUUID: string) {
this.store.dispatch( this.store.dispatch(
new DeleteUploadedFileAction(submissionId, sectionId, fileId) new DeleteUploadedFileAction(submissionId, sectionId, fileUUID)
); );
} }
} }

View File

@@ -4,7 +4,9 @@ import { hasValue } from '../shared/empty.util';
import { submissionSelector, SubmissionState } from './submission.reducers'; import { submissionSelector, SubmissionState } from './submission.reducers';
import { SubmissionObjectEntry, SubmissionSectionObject } from './objects/submission-objects.reducer'; import { SubmissionObjectEntry, SubmissionSectionObject } from './objects/submission-objects.reducer';
// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts' /**
* Export a function to return a subset of the state by key
*/
export function keySelector<T, V>(parentSelector: Selector<any, any>, subState: string, key: string): MemoizedSelector<T, V> { export function keySelector<T, V>(parentSelector: Selector<any, any>, subState: string, key: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => { return createSelector(parentSelector, (state: T) => {
if (hasValue(state) && hasValue(state[subState])) { if (hasValue(state) && hasValue(state[subState])) {
@@ -15,6 +17,9 @@ export function keySelector<T, V>(parentSelector: Selector<any, any>, subState:
}); });
} }
/**
* Export a function to return a subset of the state
*/
export function subStateSelector<T, V>(parentSelector: Selector<any, any>, subState: string): MemoizedSelector<T, V> { export function subStateSelector<T, V>(parentSelector: Selector<any, any>, subState: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => { return createSelector(parentSelector, (state: T) => {
if (hasValue(state) && hasValue(state[subState])) { if (hasValue(state) && hasValue(state[subState])) {

View File

@@ -6,21 +6,45 @@ import { SubmissionService } from './submission.service';
import { SubmissionObject } from '../core/submission/models/submission-object.model'; import { SubmissionObject } from '../core/submission/models/submission-object.model';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
/**
* Instance of SubmissionService used on SSR.
*/
@Injectable() @Injectable()
export class ServerSubmissionService extends SubmissionService { export class ServerSubmissionService extends SubmissionService {
/**
* Override createSubmission parent method to return an empty observable
*
* @return Observable<SubmissionObject>
* observable of SubmissionObject
*/
createSubmission(): Observable<SubmissionObject> { createSubmission(): Observable<SubmissionObject> {
return observableOf(null); return observableOf(null);
} }
/**
* Override retrieveSubmission parent method to return an empty observable
*
* @return Observable<SubmissionObject>
* observable of SubmissionObject
*/
retrieveSubmission(submissionId): Observable<RemoteData<SubmissionObject>> { retrieveSubmission(submissionId): Observable<RemoteData<SubmissionObject>> {
return observableOf(null); return observableOf(null);
} }
/**
* Override startAutoSave parent method and return without doing anything
*
* @param submissionId
* The submission id
*/
startAutoSave(submissionId) { startAutoSave(submissionId) {
return; return;
} }
/**
* Override startAutoSave parent method and return without doing anything
*/
stopAutoSave() { stopAutoSave() {
return; return;
} }

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