mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
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:
@@ -410,6 +410,7 @@
|
||||
"f.dateIssued.min": "Start date",
|
||||
"f.dateIssued.max": "End date",
|
||||
"f.subject": "Subject",
|
||||
"f.has_content_in_original_bundle": "Has files",
|
||||
"f.namedresourcetype": "Status",
|
||||
"f.dateSubmitted": "Date submitted",
|
||||
"f.itemtype": "Type",
|
||||
@@ -729,7 +730,7 @@
|
||||
"group-collapse": "Collapse",
|
||||
"group-expand": "Expand",
|
||||
"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": {
|
||||
}
|
||||
},
|
||||
@@ -762,6 +763,34 @@
|
||||
"chips": {
|
||||
"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": {
|
||||
"general":{
|
||||
"cannot_submit": "You have not the privilege to make a new submission.",
|
||||
@@ -822,14 +851,16 @@
|
||||
"upload-successful": "Upload successful",
|
||||
"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.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": {
|
||||
"access-condition-label": "Access condition type",
|
||||
"from-label": "Access grant from",
|
||||
"from-placeholder": "From",
|
||||
"until-label": "Access grant until",
|
||||
"until-placeholder": "Until",
|
||||
"group-label": "Group"
|
||||
"group-label": "Group",
|
||||
"group-required": "Group is required.",
|
||||
"date-required": "Date is required."
|
||||
},
|
||||
"save-metadata": "Save metadata",
|
||||
"undo": "Cancel",
|
||||
@@ -894,33 +925,5 @@
|
||||
"browse": "browse",
|
||||
"queue-lenght": "Queue length",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
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 { 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 { isAuthenticated } from '../core/auth/selectors';
|
||||
|
||||
/**
|
||||
* This component represents the login page
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-login-page',
|
||||
styleUrls: ['./login-page.component.scss'],
|
||||
templateUrl: './login-page.component.html'
|
||||
})
|
||||
export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe onDestroy
|
||||
* @type {Subscription}
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {ActivatedRoute} route
|
||||
* @param {Store<AppState>} store
|
||||
*/
|
||||
constructor(private route: ActivatedRoute,
|
||||
private store: Store<AppState>) {}
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
const queryParamsObs = this.route.queryParams;
|
||||
const authenticated = this.store.select(isAuthenticated);
|
||||
@@ -52,6 +69,9 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from subscription
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
|
@@ -13,6 +13,11 @@ export class AccessConditionOption {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@@ -6,6 +6,9 @@ import { NormalizedSubmissionFormsModel } from './normalized-config-submission-f
|
||||
import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model';
|
||||
import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model';
|
||||
|
||||
/**
|
||||
* Class to return normalized models for config objects
|
||||
*/
|
||||
export class ConfigObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||
switch (type) {
|
||||
|
@@ -68,7 +68,7 @@ import { WorkflowitemDataService } from './submission/workflowitem-data.service'
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.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 { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
||||
|
@@ -53,6 +53,14 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
|
||||
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> {
|
||||
let result: Observable<string>;
|
||||
const args = [];
|
||||
@@ -62,6 +70,14 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
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> {
|
||||
let result: Observable<string>;
|
||||
const args = [];
|
||||
@@ -77,6 +93,15 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
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> {
|
||||
|
||||
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 object search endpoint by given search method
|
||||
*
|
||||
* @param searchMethod The search method for the object
|
||||
*/
|
||||
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||
return this.halService.getEndpoint(`${this.linkPath}/search`).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
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>>> {
|
||||
|
||||
const hrefObs = this.getSearchByHref(searchMethod, options);
|
||||
|
@@ -20,7 +20,6 @@ import {
|
||||
} from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
|
||||
describe('RequestService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
|
@@ -5,8 +5,8 @@ import { Observable, race as observableRace } from 'rxjs';
|
||||
import { filter, find, mergeMap, take } from 'rxjs/operators';
|
||||
import { remove } from 'lodash';
|
||||
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
@@ -137,7 +137,6 @@ export class RequestService {
|
||||
* @param {RestRequest} request The request to send out
|
||||
* @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 {
|
||||
const isGetRequest = request.method === RestRequestMethod.GET;
|
||||
if (forceBypassCache) {
|
||||
|
@@ -1,22 +1,50 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { Group } from './group.model';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
|
||||
export class EPerson extends DSpaceObject {
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this Collection
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* A string representing the last active date for this EPerson
|
||||
*/
|
||||
public lastActive: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson can log in
|
||||
*/
|
||||
public canLogIn: boolean;
|
||||
|
||||
/**
|
||||
* The EPerson email address
|
||||
*/
|
||||
public email: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson require certificate
|
||||
*/
|
||||
public requireCertificate: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson registered itself
|
||||
*/
|
||||
public selfRegistered: boolean;
|
||||
|
||||
/** Getter to retrieve the EPerson's full name as a string */
|
||||
|
@@ -1,12 +1,28 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A string representing the name of this Group
|
||||
*/
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* A string representing the name of this Group is permanent
|
||||
*/
|
||||
public permanent: boolean;
|
||||
}
|
||||
|
@@ -1,37 +1,62 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||
import { EPerson } from './eperson.model';
|
||||
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||
import { Group } from './group.model';
|
||||
import { NormalizedGroup } from './normalized-group.model';
|
||||
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
@mapsTo(EPerson)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject, ListableObject {
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this EPerson
|
||||
*/
|
||||
@autoserialize
|
||||
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
|
||||
public netid: string;
|
||||
|
||||
/**
|
||||
* A string representing the last active date for this EPerson
|
||||
*/
|
||||
@autoserialize
|
||||
public lastActive: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson can log in
|
||||
*/
|
||||
@autoserialize
|
||||
public canLogIn: boolean;
|
||||
|
||||
/**
|
||||
* The EPerson email address
|
||||
*/
|
||||
@autoserialize
|
||||
public email: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson require certificate
|
||||
*/
|
||||
@autoserialize
|
||||
public requireCertificate: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this EPerson registered itself
|
||||
*/
|
||||
@autoserialize
|
||||
public selfRegistered: boolean;
|
||||
}
|
||||
|
@@ -1,23 +1,38 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-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 { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
@mapsTo(Group)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
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
|
||||
public handle: string;
|
||||
|
||||
/**
|
||||
* A string representing the name of this Group
|
||||
*/
|
||||
@autoserialize
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* A string representing the name of this Group is permanent
|
||||
*/
|
||||
@autoserialize
|
||||
public permanent: boolean;
|
||||
}
|
||||
|
@@ -1,29 +1,8 @@
|
||||
// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts'
|
||||
import { createSelector, MemoizedSelector, Selector } from '@ngrx/store';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { MemoizedSelector } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
import { keySelector, subStateSelector } from '../../submission/selectors';
|
||||
|
||||
/**
|
||||
* Return MemoizedSelector to select all jsonPatchOperations for a specified resource type, stored in the state
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import { Workspaceitem } from './workspaceitem.model';
|
||||
|
||||
export class EditItem extends Workspaceitem {
|
||||
}
|
@@ -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> {
|
||||
|
||||
}
|
@@ -5,22 +5,37 @@ import { Workflowitem } from './workflowitem.model';
|
||||
import { NormalizedSubmissionObject } from './normalized-submission-object.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* An model class for a NormalizedWorkflowItem.
|
||||
*/
|
||||
@mapsTo(Workflowitem)
|
||||
@inheritSerialization(NormalizedSubmissionObject)
|
||||
export class NormalizedWorkflowItem extends NormalizedSubmissionObject<Workflowitem> {
|
||||
|
||||
/**
|
||||
* The collection this workflowitem belonging to
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Collection, false)
|
||||
collection: string;
|
||||
|
||||
/**
|
||||
* The item created with this workflowitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Item, false)
|
||||
item: string;
|
||||
|
||||
/**
|
||||
* The configuration object that define this workflowitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.SubmissionDefinition, false)
|
||||
submissionDefinition: string;
|
||||
|
||||
/**
|
||||
* The EPerson who submit this workflowitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.EPerson, false)
|
||||
submitter: string;
|
||||
|
@@ -7,23 +7,38 @@ import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-obj
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { Workflowitem } from './workflowitem.model';
|
||||
|
||||
/**
|
||||
* An model class for a NormalizedWorkspaceItem.
|
||||
*/
|
||||
@mapsTo(Workspaceitem)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
@inheritSerialization(NormalizedSubmissionObject)
|
||||
export class NormalizedWorkspaceItem extends NormalizedSubmissionObject<Workflowitem> {
|
||||
|
||||
/**
|
||||
* The collection this workspaceitem belonging to
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Collection, false)
|
||||
collection: string;
|
||||
|
||||
/**
|
||||
* The item created with this workspaceitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Item, false)
|
||||
item: string;
|
||||
|
||||
/**
|
||||
* The configuration object that define this workspaceitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.SubmissionDefinition, false)
|
||||
submissionDefinition: string;
|
||||
|
||||
/**
|
||||
* The EPerson who submit this workspaceitem
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.EPerson, false)
|
||||
submitter: string;
|
||||
|
@@ -46,7 +46,7 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable
|
||||
sections: WorkspaceitemSectionsObject;
|
||||
|
||||
/**
|
||||
* The submission config definition
|
||||
* The configuration object that define this submission
|
||||
*/
|
||||
submissionDefinition: Observable<RemoteData<SubmissionDefinitionsModel>> | SubmissionDefinitionsModel;
|
||||
|
||||
|
@@ -1,7 +1,30 @@
|
||||
/**
|
||||
* An interface to represent bitstream's access condition.
|
||||
*/
|
||||
export class SubmissionUploadFileAccessConditionObject {
|
||||
|
||||
/**
|
||||
* The access condition id
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The access condition name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The access group UUID defined in this access condition
|
||||
*/
|
||||
groupUUID: string;
|
||||
|
||||
/**
|
||||
* Possible start date of the access condition
|
||||
*/
|
||||
startDate: string;
|
||||
|
||||
/**
|
||||
* Possible end date of the access condition
|
||||
*/
|
||||
endDate: string;
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { Workspaceitem } from './workspaceitem.model';
|
||||
|
||||
/**
|
||||
* A model class for a Workflowitem.
|
||||
*/
|
||||
export class Workflowitem extends Workspaceitem {
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -1,6 +1,10 @@
|
||||
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||
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 {
|
||||
[metadata: string]: FormFieldMetadataValueObject[];
|
||||
}
|
||||
|
@@ -1,5 +1,20 @@
|
||||
|
||||
/**
|
||||
* An interface to represent submission's license section data.
|
||||
*/
|
||||
export interface WorkspaceitemSectionLicenseObject {
|
||||
/**
|
||||
* The license url
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* The acceptance date of the license
|
||||
*/
|
||||
acceptanceDate: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if license has been granted
|
||||
*/
|
||||
granted: boolean;
|
||||
}
|
||||
|
@@ -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[];
|
||||
}
|
@@ -1,15 +1,46 @@
|
||||
import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model';
|
||||
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
|
||||
|
||||
/**
|
||||
* An interface to represent submission's upload section file entry.
|
||||
*/
|
||||
export class WorkspaceitemSectionUploadFileObject {
|
||||
|
||||
/**
|
||||
* The file UUID
|
||||
*/
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* The file metadata
|
||||
*/
|
||||
metadata: WorkspaceitemSectionFormObject;
|
||||
|
||||
/**
|
||||
* The file size
|
||||
*/
|
||||
sizeBytes: number;
|
||||
|
||||
/**
|
||||
* The file check sum
|
||||
*/
|
||||
checkSum: {
|
||||
checkSumAlgorithm: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The file url
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* The file thumbnail url
|
||||
*/
|
||||
thumbnail: string;
|
||||
|
||||
/**
|
||||
* The list of file access conditions
|
||||
*/
|
||||
accessConditions: SubmissionUploadFileAccessConditionObject[];
|
||||
}
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
|
||||
|
||||
/**
|
||||
* An interface to represent submission's upload section data.
|
||||
*/
|
||||
export interface WorkspaceitemSectionUploadObject {
|
||||
|
||||
/**
|
||||
* A list of [[WorkspaceitemSectionUploadFileObject]]
|
||||
*/
|
||||
files: WorkspaceitemSectionUploadFileObject[];
|
||||
}
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
|
||||
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.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 {
|
||||
[name: string]: WorkspaceitemSectionDataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a type alias of all sections
|
||||
*/
|
||||
export type WorkspaceitemSectionDataType
|
||||
= WorkspaceitemSectionUploadObject
|
||||
| WorkspaceitemSectionFormObject
|
||||
| WorkspaceitemSectionLicenseObject
|
||||
| WorkspaceitemSectionRecycleObject
|
||||
| WorkspaceitemSectionDetectDuplicateObject
|
||||
| string;
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { SubmissionObject } from './submission-object.model';
|
||||
|
||||
/**
|
||||
* A model class for a Workspaceitem.
|
||||
*/
|
||||
export class Workspaceitem extends SubmissionObject {
|
||||
|
||||
}
|
||||
|
@@ -9,6 +9,9 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon
|
||||
import { SubmissionPatchRequest } from '../data/request.models';
|
||||
import { CoreState } from '../core.reducers';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make JSON Patch requests.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||
protected linkPath = '';
|
||||
|
@@ -11,7 +11,6 @@ export enum SubmissionResourceType {
|
||||
Group = 'group',
|
||||
WorkspaceItem = 'workspaceitem',
|
||||
WorkflowItem = 'workflowitem',
|
||||
EditItem = 'edititem',
|
||||
SubmissionDefinitions = 'submissiondefinitions',
|
||||
SubmissionDefinition = 'submissiondefinition',
|
||||
SubmissionForm = 'submissionform',
|
||||
|
@@ -13,11 +13,15 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { SubmissionResourceType } from './submission-resource-type';
|
||||
import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.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 { SubmissionObject } from './models/submission-object.model';
|
||||
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 {
|
||||
return (typeof obj === 'object'
|
||||
&& obj.hasOwnProperty('value')
|
||||
@@ -27,6 +31,11 @@ export function isServerFormValue(obj: any): boolean {
|
||||
&& obj.hasOwnProperty('place'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a function to normalize sections object of the server response
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
export function normalizeSectionData(obj: any) {
|
||||
let result: any = obj;
|
||||
if (isNotNull(obj)) {
|
||||
@@ -74,6 +83,13 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data from the workspaceitems/workflowitems endpoints
|
||||
*
|
||||
* @param {RestRequest} request
|
||||
* @param {DSpaceRESTV2Response} data
|
||||
* @returns {RestResponse}
|
||||
*/
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload)
|
||||
&& 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[] {
|
||||
const dataDefinition = this.process<ObjectDomain, ObjectType>(data, requestHref);
|
||||
const normalizedDefinition = Array.of();
|
||||
@@ -103,8 +126,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
||||
let normalizedItem = Object.assign({}, item);
|
||||
// In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form
|
||||
if (item instanceof NormalizedWorkspaceItem
|
||||
|| item instanceof NormalizedWorkflowItem
|
||||
|| item instanceof NormalizedEditItem) {
|
||||
|| item instanceof NormalizedWorkflowItem) {
|
||||
if (item.sections) {
|
||||
const precessedSection = Object.create({});
|
||||
// Iterate over all workspaceitem's sections
|
||||
|
@@ -2,18 +2,18 @@ import { TestScheduler } from 'rxjs/testing';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
import { SubmissionRestService } from './submission-rest.service';
|
||||
import { RequestService } from '../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service';
|
||||
import { getMockRequestService } from '../shared/mocks/mock-request.service';
|
||||
import { getMockRemoteDataBuildService } from '../shared/mocks/mock-remote-data-build.service';
|
||||
import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import {
|
||||
SubmissionDeleteRequest,
|
||||
SubmissionPatchRequest,
|
||||
SubmissionPostRequest,
|
||||
SubmissionRequest
|
||||
} from '../core/data/request.models';
|
||||
import { FormFieldMetadataValueObject } from '../shared/form/builder/models/form-field-metadata-value.model';
|
||||
} from '../data/request.models';
|
||||
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
|
||||
|
||||
describe('SubmissionRestService test suite', () => {
|
||||
let scheduler: TestScheduler;
|
@@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
|
||||
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators';
|
||||
|
||||
import { RequestService } from '../core/data/request.service';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import {
|
||||
DeleteRequest,
|
||||
PostRequest,
|
||||
@@ -13,14 +13,17 @@ import {
|
||||
SubmissionPatchRequest,
|
||||
SubmissionPostRequest,
|
||||
SubmissionRequest
|
||||
} from '../core/data/request.models';
|
||||
import { SubmitDataResponseDefinitionObject } from '../core/shared/submit-data-response-definition.model';
|
||||
import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HALEndpointService } from '../core/shared/hal-endpoint.service';
|
||||
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service';
|
||||
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../core/cache/response.models';
|
||||
import { getResponseFromEntry } from '../core/shared/operators';
|
||||
} from '../data/request.models';
|
||||
import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
|
||||
/**
|
||||
* The service handling all submission REST requests
|
||||
*/
|
||||
@Injectable()
|
||||
export class SubmissionRestService {
|
||||
protected linkPath = 'workspaceitems';
|
||||
@@ -31,6 +34,14 @@ export class SubmissionRestService {
|
||||
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> {
|
||||
const responses = this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry()
|
||||
@@ -47,10 +58,28 @@ export class SubmissionRestService {
|
||||
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 {
|
||||
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> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
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) => new SubmissionDeleteRequest(requestId, endpointURL)),
|
||||
tap((request: DeleteRequest) => this.requestService.configure(request)),
|
||||
flatMap((request: DeleteRequest) => this.fetchRequest(requestId)),
|
||||
flatMap(() => this.fetchRequest(requestId)),
|
||||
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();
|
||||
return this.halService.getEndpoint(linkName).pipe(
|
||||
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
|
||||
@@ -71,10 +110,24 @@ export class SubmissionRestService {
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
|
||||
tap((request: RestRequest) => this.requestService.configure(request, true)),
|
||||
flatMap((request: RestRequest) => this.fetchRequest(requestId)),
|
||||
flatMap(() => this.fetchRequest(requestId)),
|
||||
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> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
return this.halService.getEndpoint(linkName).pipe(
|
||||
@@ -83,10 +136,22 @@ export class SubmissionRestService {
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)),
|
||||
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||
flatMap((request: PostRequest) => this.fetchRequest(requestId)),
|
||||
flatMap(() => this.fetchRequest(requestId)),
|
||||
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> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
return this.halService.getEndpoint(linkName).pipe(
|
||||
@@ -95,7 +160,7 @@ export class SubmissionRestService {
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new SubmissionPatchRequest(requestId, endpointURL, body)),
|
||||
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||
flatMap((request: PostRequest) => this.fetchRequest(requestId)),
|
||||
flatMap(() => this.fetchRequest(requestId)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
export enum SubmissionScopeType {
|
||||
WorkspaceItem = 'WORKSPACE',
|
||||
WorkflowItem = 'WORKFLOW',
|
||||
EditItem = 'ITEM',
|
||||
WorkflowItem = 'WORKFLOW'
|
||||
}
|
||||
|
@@ -14,6 +14,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workflowitems endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
export class WorkflowitemDataService extends DataService<Workflowitem> {
|
||||
protected linkPath = 'workflowitems';
|
||||
|
@@ -14,6 +14,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
export class WorkspaceitemDataService extends DataService<Workspaceitem> {
|
||||
protected linkPath = 'workspaceitems';
|
||||
|
@@ -2,6 +2,9 @@ import { ServerResponseService } from '../shared/services/server-response.servic
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component representing the `PageNotFound` DSpace page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-pagenotfound',
|
||||
styleUrls: ['./pagenotfound.component.scss'],
|
||||
@@ -9,10 +12,20 @@ import { AuthService } from '../core/auth/auth.service';
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class PageNotFoundComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {AuthService} authservice
|
||||
* @param {ServerResponseService} responseService
|
||||
*/
|
||||
constructor(private authservice: AuthService, private responseService: ServerResponseService) {
|
||||
this.responseService.setNotFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove redirect url from the state
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.authservice.clearRedirectUrl();
|
||||
}
|
||||
|
114
src/app/shared/alert/alert.component.spec.ts
Normal file
114
src/app/shared/alert/alert.component.spec.ts
Normal 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;
|
||||
}
|
@@ -1,9 +1,12 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { trigger } from '@angular/animations';
|
||||
|
||||
import { AlertType } from './aletrs-type';
|
||||
import { AlertType } from './aletr-type';
|
||||
import { fadeOutLeave, fadeOutState } from '../animations/fade';
|
||||
|
||||
/**
|
||||
* This component allow to create div that uses the Bootstrap's Alerts component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-alert',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
@@ -12,23 +15,52 @@ import { fadeOutLeave, fadeOutState } from '../animations/fade';
|
||||
fadeOutLeave, fadeOutState,
|
||||
])
|
||||
],
|
||||
templateUrl: './alerts.component.html',
|
||||
styleUrls: ['./alerts.component.scss']
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['./alert.component.scss']
|
||||
})
|
||||
export class AlertComponent {
|
||||
|
||||
export class AlertsComponent {
|
||||
|
||||
/**
|
||||
* The alert content
|
||||
*/
|
||||
@Input() content: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if alert is dismissible
|
||||
*/
|
||||
@Input() dismissible = false;
|
||||
|
||||
/**
|
||||
* The alert type
|
||||
*/
|
||||
@Input() type: AlertType;
|
||||
|
||||
/**
|
||||
* An event fired when alert is dismissed.
|
||||
*/
|
||||
@Output() close: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The initial animation name
|
||||
*/
|
||||
public animate = 'fadeIn';
|
||||
|
||||
/**
|
||||
* A boolean representing if alert is dismissed or not
|
||||
*/
|
||||
public dismissed = false;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {ChangeDetectorRef} cdr
|
||||
*/
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss div with animation
|
||||
*/
|
||||
dismiss() {
|
||||
if (this.dismissible) {
|
||||
this.animate = 'fadeOut';
|
@@ -19,25 +19,55 @@ import { isNotEmpty, isNull } from '../empty.util';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { ConfidenceIconConfig } from '../../../config/submission-config.interface';
|
||||
|
||||
/**
|
||||
* Directive to add to the element a bootstrap utility class based on metadata confidence value
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[dsAuthorityConfidenceState]'
|
||||
})
|
||||
export class AuthorityConfidenceStateDirective implements OnChanges {
|
||||
|
||||
/**
|
||||
* The metadata value
|
||||
*/
|
||||
@Input() authorityValue: AuthorityValue | FormFieldMetadataValueObject | string;
|
||||
|
||||
/**
|
||||
* A boolean representing if to show html icon if authority value is empty
|
||||
*/
|
||||
@Input() visibleWhenAuthorityEmpty = true;
|
||||
|
||||
/**
|
||||
* The css class applied before directive changes
|
||||
*/
|
||||
private previousClass: string = null;
|
||||
|
||||
/**
|
||||
* The css class applied after directive changes
|
||||
*/
|
||||
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>();
|
||||
|
||||
/**
|
||||
* Listener to click event
|
||||
*/
|
||||
@HostListener('click') onClick() {
|
||||
if (isNotEmpty(this.authorityValue) && this.getConfidenceByValue(this.authorityValue) !== ConfidenceType.CF_ACCEPTED) {
|
||||
this.whenClickOnConfidenceNotAccepted.emit(this.getConfidenceByValue(this.authorityValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {GlobalConfig} EnvConfig
|
||||
* @param {ElementRef} elem
|
||||
* @param {Renderer2} renderer
|
||||
*/
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
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 {
|
||||
if (!changes.authorityValue.firstChange) {
|
||||
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() {
|
||||
if (isNull(this.previousClass)) {
|
||||
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 {
|
||||
let confidence: ConfidenceType = ConfidenceType.CF_UNSET;
|
||||
|
||||
@@ -82,6 +125,11 @@ export class AuthorityConfidenceStateDirective implements OnChanges {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the properly css class based on confidence value
|
||||
*
|
||||
* @param confidence
|
||||
*/
|
||||
private getClassByConfidence(confidence: any): string {
|
||||
if (!this.visibleWhenAuthorityEmpty && confidence === ConfidenceType.CF_UNSET) {
|
||||
return 'd-none';
|
||||
|
@@ -131,7 +131,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-form-control',
|
||||
selector: 'ds-dynamic-form-control-container',
|
||||
styleUrls: ['./ds-dynamic-form-control-container.component.scss'],
|
||||
templateUrl: './ds-dynamic-form-control-container.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
@@ -180,9 +180,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
if (changes) {
|
||||
super.ngOnChanges(changes);
|
||||
if (this.model && this.model.placeholder) {
|
||||
this.translateService.get(this.model.placeholder).subscribe((placeholder) => {
|
||||
this.model.placeholder = placeholder;
|
||||
})
|
||||
this.model.placeholder = this.translateService.instant(this.model.placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<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"
|
||||
[group]="formGroup"
|
||||
[hasErrorMessaging]="model.hasErrorMessages"
|
||||
@@ -9,4 +9,4 @@
|
||||
[templates]="templates"
|
||||
(dfBlur)="onEvent($event, 'blur')"
|
||||
(dfChange)="onEvent($event, 'change')"
|
||||
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control>
|
||||
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control-container>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
<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"
|
||||
[context]="groupModel"
|
||||
[group]="control.at(idx)"
|
||||
@@ -21,7 +21,7 @@
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($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>
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-date-picker-inline',
|
||||
selector: 'ds-dynamic-date-picker-inline',
|
||||
templateUrl: './dynamic-date-picker-inline.component.html'
|
||||
})
|
||||
export class DsDatePickerInlineComponent extends DynamicFormControlComponent {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
[formGroupName]="model.id"
|
||||
[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"
|
||||
[formId]="formId"
|
||||
[group]="control"
|
||||
@@ -18,7 +18,7 @@
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($event)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control>
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { map, distinctUntilChanged, filter } from 'rxjs/operators';
|
||||
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 { select, Store } from '@ngrx/store';
|
||||
|
||||
@@ -82,12 +82,13 @@ export class FormService {
|
||||
/**
|
||||
* Method to validate form's fields
|
||||
*/
|
||||
public validateAllFormFields(formGroup: FormGroup) {
|
||||
public validateAllFormFields(formGroup: FormGroup | FormArray) {
|
||||
Object.keys(formGroup.controls).forEach((field) => {
|
||||
const control = formGroup.get(field);
|
||||
if (control instanceof FormControl) {
|
||||
control.markAsTouched({ onlySelf: true });
|
||||
} else if (control instanceof FormGroup) {
|
||||
control.markAsDirty({ onlySelf: true });
|
||||
} else if (control instanceof FormGroup || control instanceof FormArray) {
|
||||
this.validateAllFormFields(control);
|
||||
}
|
||||
});
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { SectionFormOperationsService } from '../../submission/sections/form/section-form-operations.service';
|
||||
|
||||
/**
|
||||
* Mock for [[FormOperationsService]]
|
||||
*/
|
||||
export function getMockFormOperationsService(): SectionFormOperationsService {
|
||||
return jasmine.createSpyObj('SectionFormOperationsService', {
|
||||
dispatchOperationsFromEvent: jasmine.createSpy('dispatchOperationsFromEvent'),
|
||||
|
@@ -2,6 +2,9 @@ import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { FormService } from '../form/form.service';
|
||||
|
||||
/**
|
||||
* Mock for [[FormService]]
|
||||
*/
|
||||
export function getMockFormService(
|
||||
id$: string = 'random_id'
|
||||
): FormService {
|
||||
@@ -12,8 +15,8 @@ export function getMockFormService(
|
||||
getForm: observableOf({}),
|
||||
getUniqueId: id$,
|
||||
resetForm: {},
|
||||
validateAllFormFields: {},
|
||||
isValid: observableOf(true),
|
||||
validateAllFormFields: jasmine.createSpy('validateAllFormFields'),
|
||||
isValid: jasmine.createSpy('isValid'),
|
||||
isFormInitialized: observableOf(true)
|
||||
});
|
||||
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Mock for [[RouterService]]
|
||||
*/
|
||||
export class MockRouter {
|
||||
public events = observableOf({});
|
||||
public routerState = {
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
/**
|
||||
* Mock for [[ScrollToService]]
|
||||
*/
|
||||
export function getMockScrollToService(): ScrollToService {
|
||||
return jasmine.createSpyObj('scrollToService', {
|
||||
scrollTo: jasmine.createSpy('scrollTo')
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
|
||||
|
||||
/**
|
||||
* Mock for [[SubmissionFormsConfigService]]
|
||||
*/
|
||||
export function getMockSectionUploadService(): SubmissionFormsConfigService {
|
||||
return jasmine.createSpyObj('SectionUploadService', {
|
||||
getUploadedFileList: jasmine.createSpy('getUploadedFileList'),
|
||||
|
@@ -3,6 +3,7 @@ import { SubmissionDefinitionsModel } from '../../core/config/models/config-subm
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
|
||||
import { Group } from '../../core/eperson/models/group.model';
|
||||
|
||||
export const mockSectionsData = {
|
||||
traditionalpageone:{
|
||||
@@ -1364,7 +1365,7 @@ export const mockAccessConditionOptions = [
|
||||
}
|
||||
];
|
||||
|
||||
export const mockGroup = {
|
||||
export const mockGroup = Object.assign(new Group(), {
|
||||
handle: null,
|
||||
permanent: true,
|
||||
self: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1',
|
||||
@@ -1386,7 +1387,7 @@ export const mockGroup = {
|
||||
},
|
||||
page: []
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const mockUploadFiles = [
|
||||
{
|
||||
|
@@ -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 { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
|
||||
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 { MessageBoardComponent } from './message-board/message-board.component';
|
||||
import { MessageComponent } from './message-board/message/message.component';
|
||||
@@ -180,7 +180,7 @@ const PIPES = [
|
||||
|
||||
const COMPONENTS = [
|
||||
// put shared components here
|
||||
AlertsComponent,
|
||||
AlertComponent,
|
||||
AuthNavMenuComponent,
|
||||
UserMenuComponent,
|
||||
ChipsComponent,
|
||||
|
@@ -1,5 +1,13 @@
|
||||
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({
|
||||
name: 'dsObjNgFor'
|
||||
})
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="submission-submit-container" >
|
||||
<ds-submission-submit-form [collectionId]="collectionId"
|
||||
<ds-submission-form [collectionId]="collectionId"
|
||||
[sections]="sections"
|
||||
[selfUrl]="selfUrl"
|
||||
[submissionDefinition]="submissionDefinition"
|
||||
[submissionId]="submissionId"></ds-submission-submit-form>
|
||||
[submissionId]="submissionId"></ds-submission-form>
|
||||
</div>
|
||||
|
@@ -14,17 +14,44 @@ import { SubmissionObject } from '../../core/submission/models/submission-object
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
/**
|
||||
* This component allows to edit an existing workspaceitem/workflowitem.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-edit',
|
||||
styleUrls: ['./submission-edit.component.scss'],
|
||||
templateUrl: './submission-edit.component.html'
|
||||
})
|
||||
|
||||
export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* The collection id this submission belonging to
|
||||
* @type {string}
|
||||
*/
|
||||
public collectionId: string;
|
||||
|
||||
/**
|
||||
* The list of submission's sections
|
||||
* @type {WorkspaceitemSectionsObject}
|
||||
*/
|
||||
public sections: WorkspaceitemSectionsObject;
|
||||
|
||||
/**
|
||||
* The submission self url
|
||||
* @type {string}
|
||||
*/
|
||||
public selfUrl: string;
|
||||
|
||||
/**
|
||||
* The configuration object that define this submission
|
||||
* @type {SubmissionDefinitionsModel}
|
||||
*/
|
||||
public submissionDefinition: SubmissionDefinitionsModel;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
public submissionId: string;
|
||||
|
||||
/**
|
||||
@@ -33,6 +60,16 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
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,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -41,6 +78,9 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
private translate: TranslateService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve workspaceitem/workflowitem from server and initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.subs.push(this.route.paramMap.pipe(
|
||||
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() {
|
||||
this.subs
|
||||
|
@@ -36,24 +36,48 @@ import { SubmissionService } from '../../submission.service';
|
||||
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
|
||||
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
||||
|
||||
/**
|
||||
* An interface to represent a collection entry
|
||||
*/
|
||||
interface CollectionListEntryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent an entry in the collection list
|
||||
*/
|
||||
interface CollectionListEntry {
|
||||
communities: CollectionListEntryItem[],
|
||||
collection: CollectionListEntryItem
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to show the current collection the submission belonging to and to change it.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-form-collection',
|
||||
styleUrls: ['./submission-form-collection.component.scss'],
|
||||
templateUrl: './submission-form-collection.component.html'
|
||||
})
|
||||
export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
|
||||
/**
|
||||
* The current collection id this submission belonging to
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() currentCollectionId: string;
|
||||
|
||||
/**
|
||||
* The current configuration object that define this submission
|
||||
* @type {SubmissionDefinitionsModel}
|
||||
*/
|
||||
@Input() currentDefinition: string;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() submissionId;
|
||||
|
||||
/**
|
||||
@@ -62,18 +86,69 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
@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 model: any;
|
||||
|
||||
/**
|
||||
* The search form control
|
||||
* @type {FormControl}
|
||||
*/
|
||||
public searchField: FormControl = new FormControl();
|
||||
|
||||
/**
|
||||
* The collection list obtained from a search
|
||||
* @type {Observable<CollectionListEntry[]>}
|
||||
*/
|
||||
public searchListCollection$: Observable<CollectionListEntry[]>;
|
||||
|
||||
/**
|
||||
* The selected collection id
|
||||
* @type {string}
|
||||
*/
|
||||
public selectedCollectionId: string;
|
||||
|
||||
/**
|
||||
* The selected collection name
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public selectedCollectionName$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The JsonPatchOperationPathCombiner object
|
||||
* @type {JsonPatchOperationPathCombiner}
|
||||
*/
|
||||
protected pathCombiner: JsonPatchOperationPathCombiner;
|
||||
|
||||
/**
|
||||
* A boolean representing if dropdown list is scrollable to the bottom
|
||||
* @type {boolean}
|
||||
*/
|
||||
private scrollableBottom = false;
|
||||
|
||||
/**
|
||||
* A boolean representing if dropdown list is scrollable to the top
|
||||
* @type {boolean}
|
||||
*/
|
||||
private scrollableTop = false;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
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,
|
||||
private communityDataService: CommunityDataService,
|
||||
private operationsBuilder: JsonPatchOperationsBuilder,
|
||||
@@ -81,6 +156,13 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
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) {
|
||||
if (event.wheelDelta > 0 && this.scrollableTop) {
|
||||
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) {
|
||||
this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight);
|
||||
this.scrollableTop = (event.target.scrollTop === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize collection list
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (hasValue(changes.currentCollectionId)
|
||||
&& hasValue(changes.currentCollectionId.currentValue)) {
|
||||
@@ -153,14 +243,26 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
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) {
|
||||
this.searchField.reset();
|
||||
this.disabled$.next(true);
|
||||
@@ -181,10 +283,19 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset search form control on dropdown menu close
|
||||
*/
|
||||
onClose() {
|
||||
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) {
|
||||
if (!isOpen) {
|
||||
this.searchField.reset();
|
||||
|
@@ -13,7 +13,7 @@ import { mockSubmissionId } from '../../../shared/mocks/mock-submission';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
import { SubmissionRestServiceStub } from '../../../shared/testing/submission-rest-service-stub';
|
||||
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';
|
||||
|
||||
describe('SubmissionFormFooterComponent Component', () => {
|
||||
|
@@ -4,11 +4,14 @@ import { Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
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 { SubmissionScopeType } from '../../../core/submission/submission-scope-type';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This component represents submission form footer bar.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-form-footer',
|
||||
styleUrls: ['./submission-form-footer.component.scss'],
|
||||
@@ -16,18 +19,51 @@ import { isNotEmpty } from '../../../shared/empty.util';
|
||||
})
|
||||
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>;
|
||||
|
||||
/**
|
||||
* A boolean representing if a submission save operation is pending
|
||||
* @type {Observable<boolean>}
|
||||
*/
|
||||
public processingSaveStatus: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* A boolean representing if showing deposit and discard buttons
|
||||
* @type {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);
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {NgbModal} modalService
|
||||
* @param {SubmissionRestService} restService
|
||||
* @param {SubmissionService} submissionService
|
||||
*/
|
||||
constructor(private modalService: NgbModal,
|
||||
private restService: SubmissionRestService,
|
||||
private submissionService: SubmissionService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (isNotEmpty(this.submissionId)) {
|
||||
this.submissionIsInvalid = this.submissionService.getSubmissionStatus(this.submissionId).pipe(
|
||||
@@ -40,18 +76,30 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submission save action
|
||||
*/
|
||||
save(event) {
|
||||
this.submissionService.dispatchSave(this.submissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submission save for later action
|
||||
*/
|
||||
saveLater(event) {
|
||||
this.submissionService.dispatchSaveForLater(this.submissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submission deposit action
|
||||
*/
|
||||
public deposit(event) {
|
||||
this.submissionService.dispatchDeposit(this.submissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submission discard action
|
||||
*/
|
||||
public confirmDiscard(content) {
|
||||
this.modalService.open(content).result.then(
|
||||
(result) => {
|
||||
|
@@ -1,30 +1,62 @@
|
||||
import { Component, Input, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { SectionsService } from '../../sections/sections.service';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
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({
|
||||
selector: 'ds-submission-form-section-add',
|
||||
styleUrls: [ './submission-form-section-add.component.scss' ],
|
||||
templateUrl: './submission-form-section-add.component.html'
|
||||
})
|
||||
export class SubmissionFormSectionAddComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The collection id this submission belonging to
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() collectionId: string;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() submissionId: string;
|
||||
|
||||
/**
|
||||
* The possible section list to add
|
||||
* @type {Observable<SectionDataObject[]>}
|
||||
*/
|
||||
public sectionList$: Observable<SectionDataObject[]>;
|
||||
|
||||
/**
|
||||
* A boolean representing if there are available sections to add
|
||||
* @type {Observable<boolean>}
|
||||
*/
|
||||
public hasSections$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {SectionsService} sectionService
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {HostWindowService} windowService
|
||||
*/
|
||||
constructor(private sectionService: SectionsService,
|
||||
private submissionService: SubmissionService,
|
||||
public windowService: HostWindowService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.sectionList$ = this.submissionService.getDisabledSectionsList(this.submissionId);
|
||||
this.hasSections$ = this.sectionList$.pipe(
|
||||
@@ -32,6 +64,9 @@ export class SubmissionFormSectionAddComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an action to add a new section
|
||||
*/
|
||||
addSection(sectionId) {
|
||||
this.sectionService.addSection(this.submissionId, sectionId);
|
||||
}
|
||||
|
@@ -24,9 +24,9 @@
|
||||
<div class="submission-form-content">
|
||||
<ds-loading *ngIf="(isLoading() | async)" message="Loading..."></ds-loading>
|
||||
<ng-container *ngFor="let object of (submissionSections | async)">
|
||||
<ds-submission-form-section-container [collectionId]="collectionId"
|
||||
<ds-submission-section-container [collectionId]="collectionId"
|
||||
[submissionId]="submissionId"
|
||||
[sectionData]="object"></ds-submission-form-section-container>
|
||||
[sectionData]="object"></ds-submission-section-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="!(isLoading() | async)" class="submission-form-footer mt-3 mb-3 position-sticky">
|
||||
|
@@ -63,10 +63,10 @@ describe('SubmissionFormComponent Component', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `
|
||||
<ds-submission-submit-form [collectionId]="collectionId"
|
||||
<ds-submission-form [collectionId]="collectionId"
|
||||
[selfUrl]="selfUrl"
|
||||
[submissionDefinition]="submissionDefinition"
|
||||
[submissionId]="submissionId"></ds-submission-submit-form>`;
|
||||
[submissionId]="submissionId"></ds-submission-form>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
|
@@ -15,22 +15,68 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { SubmissionObject } from '../../core/submission/models/submission-object.model';
|
||||
|
||||
/**
|
||||
* This component represents the submission form.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-submit-form',
|
||||
selector: 'ds-submission-form',
|
||||
styleUrls: ['./submission-form.component.scss'],
|
||||
templateUrl: './submission-form.component.html',
|
||||
})
|
||||
export class SubmissionFormComponent implements OnChanges, OnDestroy {
|
||||
|
||||
/**
|
||||
* The collection id this submission belonging to
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() collectionId: string;
|
||||
|
||||
/**
|
||||
* The list of submission's sections
|
||||
* @type {WorkspaceitemSectionsObject}
|
||||
*/
|
||||
@Input() sections: WorkspaceitemSectionsObject;
|
||||
|
||||
/**
|
||||
* The submission self url
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() selfUrl: string;
|
||||
|
||||
/**
|
||||
* The configuration object that define this submission
|
||||
* @type {SubmissionDefinitionsModel}
|
||||
*/
|
||||
@Input() submissionDefinition: SubmissionDefinitionsModel;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() submissionId: string;
|
||||
|
||||
/**
|
||||
* The configuration id that define this submission
|
||||
* @type {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 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 = {
|
||||
url: '',
|
||||
authToken: null,
|
||||
@@ -38,9 +84,26 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
|
||||
itemAlias: null
|
||||
};
|
||||
|
||||
/**
|
||||
* A boolean representing if component is active
|
||||
* @type {boolean}
|
||||
*/
|
||||
protected isActive: boolean;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {AuthService} authService
|
||||
* @param {ChangeDetectorRef} changeDetectorRef
|
||||
* @param {HALEndpointService} halService
|
||||
* @param {SubmissionService} submissionService
|
||||
*/
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@@ -49,9 +112,14 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables and retrieve form configuration
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.collectionId && this.submissionId) {
|
||||
this.isActive = true;
|
||||
|
||||
// retrieve submission's section list
|
||||
this.submissionSections = this.submissionService.getSubmissionObject(this.submissionId).pipe(
|
||||
filter(() => this.isActive),
|
||||
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(
|
||||
filter(() => this.isActive),
|
||||
map((submission: SubmissionObjectEntry) => submission.isLoading),
|
||||
map((isLoading: boolean) => isLoading),
|
||||
distinctUntilChanged());
|
||||
|
||||
// init submission state
|
||||
this.subs.push(
|
||||
this.halService.getEndpoint('workspaceitems').pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
@@ -89,10 +159,16 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
);
|
||||
|
||||
// start auto save
|
||||
this.submissionService.startAutoSave(this.submissionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions, destroy instance variables
|
||||
* and reset submission state
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.isActive = false;
|
||||
this.submissionService.stopAutoSave();
|
||||
@@ -102,6 +178,13 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
|
||||
.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) {
|
||||
this.collectionId = (submissionObject.collection as Collection).id;
|
||||
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> {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if submission form is loading
|
||||
*/
|
||||
protected getSectionsList(): Observable<any> {
|
||||
return this.submissionService.getSubmissionSections(this.submissionId).pipe(
|
||||
filter((sections: SectionDataObject[]) => isNotEmpty(sections)),
|
||||
|
@@ -14,24 +14,72 @@ import { UploaderOptions } from '../../../shared/uploader/uploader-options.model
|
||||
import parseSectionErrors from '../../utils/parseSectionErrors';
|
||||
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({
|
||||
selector: 'ds-submission-upload-files',
|
||||
templateUrl: './submission-upload-files.component.html',
|
||||
})
|
||||
export class SubmissionUploadFilesComponent implements OnChanges {
|
||||
|
||||
@Input() collectionId;
|
||||
@Input() submissionId;
|
||||
@Input() sectionId;
|
||||
/**
|
||||
* The collection id this submission belonging to
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* A boolean representing if is possible to active drop zone over the document page
|
||||
* @type {boolean}
|
||||
*/
|
||||
public enableDragOverDocument = true;
|
||||
|
||||
/**
|
||||
* i18n message label
|
||||
* @type {string}
|
||||
*/
|
||||
public dropOverDocumentMsg = 'submission.sections.upload.drop-message';
|
||||
|
||||
/**
|
||||
* i18n message label
|
||||
* @type {string}
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* Save submission before to upload a file
|
||||
*/
|
||||
public onBeforeUpload = () => {
|
||||
const sub: Subscription = this.operationsService.jsonPatchByResourceType(
|
||||
this.submissionService.getSubmissionObjectLinkName(),
|
||||
@@ -42,6 +90,15 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
||||
return sub;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {NotificationsService} notificationsService
|
||||
* @param {SubmissionJsonPatchOperationsService} operationsService
|
||||
* @param {SectionsService} sectionService
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {TranslateService} translate
|
||||
*/
|
||||
constructor(private notificationsService: NotificationsService,
|
||||
private operationsService: SubmissionJsonPatchOperationsService,
|
||||
private sectionService: SectionsService,
|
||||
@@ -49,10 +106,19 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
||||
private translate: TranslateService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if upload functionality is enabled
|
||||
*/
|
||||
ngOnChanges() {
|
||||
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) {
|
||||
// Checks if upload section is enabled so do upload
|
||||
this.subs.push(
|
||||
@@ -61,8 +127,8 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
||||
.subscribe((isUploadEnabled) => {
|
||||
if (isUploadEnabled) {
|
||||
|
||||
const {sections} = workspaceitem;
|
||||
const {errors} = workspaceitem;
|
||||
const { sections } = workspaceitem;
|
||||
const { errors } = workspaceitem;
|
||||
|
||||
const errorsList = parseSectionErrors(errors);
|
||||
if (sections && isNotEmpty(sections)) {
|
||||
@@ -87,12 +153,15 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error notification on upload fails
|
||||
*/
|
||||
public onUploadError() {
|
||||
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() {
|
||||
this.subs
|
||||
|
@@ -24,7 +24,7 @@ import {
|
||||
SaveSubmissionFormSuccessAction,
|
||||
SaveSubmissionSectionFormAction,
|
||||
SaveSubmissionSectionFormErrorAction,
|
||||
SaveSubmissionSectionFormSuccessAction,
|
||||
SaveSubmissionSectionFormSuccessAction, SubmissionObjectAction,
|
||||
SubmissionObjectActionTypes,
|
||||
UpdateSectionDataAction
|
||||
} from './submission-objects.actions';
|
||||
@@ -48,6 +48,9 @@ import { SubmissionJsonPatchOperationsService } from '../../core/submission/subm
|
||||
@Injectable()
|
||||
export class SubmissionObjectEffects {
|
||||
|
||||
/**
|
||||
* Dispatch a [InitSectionAction] for every submission sections and dispatch a [CompleteInitSubmissionFormAction]
|
||||
*/
|
||||
@Effect() loadForm$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.INIT_SUBMISSION_FORM),
|
||||
map((action: InitSubmissionFormAction) => {
|
||||
@@ -83,6 +86,9 @@ export class SubmissionObjectEffects {
|
||||
));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Dispatch a [InitSubmissionFormAction]
|
||||
*/
|
||||
@Effect() resetForm$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.RESET_SUBMISSION_FORM),
|
||||
map((action: ResetSubmissionFormAction) =>
|
||||
@@ -95,6 +101,9 @@ export class SubmissionObjectEffects {
|
||||
null
|
||||
)));
|
||||
|
||||
/**
|
||||
* Dispatch a [SaveSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error
|
||||
*/
|
||||
@Effect() saveSubmission$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM),
|
||||
switchMap((action: SaveSubmissionFormAction) => {
|
||||
@@ -106,6 +115,9 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Dispatch a [SaveForLaterSubmissionFormSuccessAction] or a [SaveSubmissionFormErrorAction] on error
|
||||
*/
|
||||
@Effect() saveForLaterSubmission$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM),
|
||||
switchMap((action: SaveForLaterSubmissionFormAction) => {
|
||||
@@ -117,6 +129,9 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Call parseSaveResponse and dispatch actions
|
||||
*/
|
||||
@Effect() saveSubmissionSuccess$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS),
|
||||
withLatestFrom(this.store$),
|
||||
@@ -125,6 +140,9 @@ export class SubmissionObjectEffects {
|
||||
}),
|
||||
mergeMap((actions) => observableFrom(actions)));
|
||||
|
||||
/**
|
||||
* Dispatch a [SaveSubmissionSectionFormSuccessAction] or a [SaveSubmissionSectionFormErrorAction] on error
|
||||
*/
|
||||
@Effect() saveSection$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM),
|
||||
switchMap((action: SaveSubmissionSectionFormAction) => {
|
||||
@@ -137,11 +155,17 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Show a notification on error
|
||||
*/
|
||||
@Effect({dispatch: false}) saveError$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR),
|
||||
withLatestFrom(this.store$),
|
||||
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(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION),
|
||||
withLatestFrom(this.store$),
|
||||
@@ -161,6 +185,9 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Dispatch a [DepositSubmissionSuccessAction] or a [DepositSubmissionErrorAction] on error
|
||||
*/
|
||||
@Effect() depositSubmission$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION),
|
||||
withLatestFrom(this.store$),
|
||||
@@ -170,20 +197,32 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Show a notification on success and redirect to MyDSpace page
|
||||
*/
|
||||
@Effect({dispatch: false}) saveForLaterSubmissionSuccess$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS),
|
||||
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))),
|
||||
tap(() => this.submissionService.redirectToMyDSpace()));
|
||||
|
||||
/**
|
||||
* Show a notification on success and redirect to MyDSpace page
|
||||
*/
|
||||
@Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS),
|
||||
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))),
|
||||
tap(() => this.submissionService.redirectToMyDSpace()));
|
||||
|
||||
/**
|
||||
* Show a notification on error
|
||||
*/
|
||||
@Effect({dispatch: false}) depositSubmissionError$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR),
|
||||
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(
|
||||
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION),
|
||||
switchMap((action: DepositSubmissionAction) => {
|
||||
@@ -192,11 +231,17 @@ export class SubmissionObjectEffects {
|
||||
catchError(() => observableOf(new DiscardSubmissionErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
/**
|
||||
* Show a notification on success and redirect to MyDSpace page
|
||||
*/
|
||||
@Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS),
|
||||
tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))),
|
||||
tap(() => this.submissionService.redirectToMyDSpace()));
|
||||
|
||||
/**
|
||||
* Show a notification on error
|
||||
*/
|
||||
@Effect({dispatch: false}) discardSubmissionError$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR),
|
||||
tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice'))));
|
||||
@@ -210,6 +255,12 @@ export class SubmissionObjectEffects {
|
||||
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[]) {
|
||||
let canDeposit = true;
|
||||
|
||||
@@ -225,7 +276,26 @@ export class SubmissionObjectEffects {
|
||||
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 = [];
|
||||
|
||||
if (isNotEmpty(response)) {
|
||||
|
@@ -38,42 +38,138 @@ import { WorkspaceitemSectionDataType } from '../../core/submission/models/works
|
||||
import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model';
|
||||
import { SectionsType } from '../sections/sections-type';
|
||||
|
||||
/**
|
||||
* An interface to represent section visibility
|
||||
*/
|
||||
export interface SectionVisibility {
|
||||
main: any;
|
||||
other: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent section object state
|
||||
*/
|
||||
export interface SubmissionSectionObject {
|
||||
/**
|
||||
* The section header
|
||||
*/
|
||||
header: string;
|
||||
|
||||
/**
|
||||
* The section configuration url
|
||||
*/
|
||||
config: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is mandatory
|
||||
*/
|
||||
mandatory: boolean;
|
||||
|
||||
/**
|
||||
* The section type
|
||||
*/
|
||||
sectionType: SectionsType;
|
||||
|
||||
/**
|
||||
* The section visibility
|
||||
*/
|
||||
visibility: SectionVisibility;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is collapsed
|
||||
*/
|
||||
collapsed: boolean,
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* The section data object
|
||||
*/
|
||||
data: WorkspaceitemSectionDataType;
|
||||
|
||||
/**
|
||||
* The list of the section errors
|
||||
*/
|
||||
errors: SubmissionSectionError[];
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is loading
|
||||
*/
|
||||
isLoading: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is valid
|
||||
*/
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent section error
|
||||
*/
|
||||
export interface SubmissionSectionError {
|
||||
/**
|
||||
* A string representing error path
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* The error message
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent SubmissionSectionObject entry
|
||||
*/
|
||||
export interface SubmissionSectionEntry {
|
||||
[sectionId: string]: SubmissionSectionObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent submission object state
|
||||
*/
|
||||
export interface SubmissionObjectEntry {
|
||||
/**
|
||||
* The collection this submission belonging to
|
||||
*/
|
||||
collection?: string,
|
||||
|
||||
/**
|
||||
* The configuration name that define this submission
|
||||
*/
|
||||
definition?: string,
|
||||
|
||||
/**
|
||||
* The submission self url
|
||||
*/
|
||||
selfUrl?: string;
|
||||
|
||||
/**
|
||||
* The submission active section
|
||||
*/
|
||||
activeSection?: string;
|
||||
|
||||
/**
|
||||
* The list of submission's sections
|
||||
*/
|
||||
sections?: SubmissionSectionEntry;
|
||||
|
||||
/**
|
||||
* A boolean representing if this submission is loading
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if a submission save operation is pending
|
||||
*/
|
||||
savePending?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if a submission deposit operation is pending
|
||||
*/
|
||||
depositPending?: boolean;
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import { of as observableOf } from 'rxjs';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
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 { SectionsType } from '../sections-type';
|
||||
import { SectionsDirective } from '../sections.directive';
|
||||
@@ -41,11 +41,11 @@ const sectionObject: SectionDataObject = {
|
||||
sectionType: SectionsType.SubmissionForm
|
||||
};
|
||||
|
||||
describe('SectionContainerComponent test suite', () => {
|
||||
describe('SubmissionSectionContainerComponent test suite', () => {
|
||||
|
||||
let comp: SectionContainerComponent;
|
||||
let comp: SubmissionSectionContainerComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<SectionContainerComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionContainerComponent>;
|
||||
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let sectionsServiceStub: SectionsServiceStub;
|
||||
@@ -71,14 +71,14 @@ describe('SectionContainerComponent test suite', () => {
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
SectionContainerComponent,
|
||||
SubmissionSectionContainerComponent,
|
||||
SectionsDirective,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
{ provide: SectionsService, useClass: SectionsServiceStub },
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
SectionContainerComponent
|
||||
SubmissionSectionContainerComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -93,16 +93,16 @@ describe('SectionContainerComponent test suite', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-submission-form-section-container [collectionId]="collectionId"
|
||||
<ds-submission-section-container [collectionId]="collectionId"
|
||||
[submissionId]="submissionId"
|
||||
[sectionData]="object"></ds-submission-form-section-container>`;
|
||||
[sectionData]="object"></ds-submission-section-container>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
init();
|
||||
});
|
||||
|
||||
it('should create SectionContainerComponent', inject([SectionContainerComponent], (app: SectionContainerComponent) => {
|
||||
it('should create SubmissionSectionContainerComponent', inject([SubmissionSectionContainerComponent], (app: SubmissionSectionContainerComponent) => {
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
@@ -110,7 +110,7 @@ describe('SectionContainerComponent test suite', () => {
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
init();
|
||||
fixture = TestBed.createComponent(SectionContainerComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionContainerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
comp.submissionId = submissionId;
|
||||
|
@@ -3,29 +3,64 @@ import { Component, Injector, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { SectionsDirective } from '../sections.directive';
|
||||
import { SectionDataObject } from '../models/section-data.model';
|
||||
import { rendersSectionType } from '../sections-decorator';
|
||||
import { SectionsType } from '../sections-type';
|
||||
import { AlertType } from '../../../shared/alerts/aletrs-type';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
|
||||
/**
|
||||
* This component represents a section that contains the submission license form.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-form-section-container',
|
||||
selector: 'ds-submission-section-container',
|
||||
templateUrl: './section-container.component.html',
|
||||
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;
|
||||
|
||||
/**
|
||||
* The section data
|
||||
* @type {SectionDataObject}
|
||||
*/
|
||||
@Input() sectionData: SectionDataObject;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
@Input() submissionId: string;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {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;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {Injector} injector
|
||||
*/
|
||||
constructor(private injector: Injector) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.objectInjector = Injector.create({
|
||||
providers: [
|
||||
@@ -37,12 +72,21 @@ export class SectionContainerComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove section from submission form
|
||||
*
|
||||
* @param event
|
||||
* the event emitted
|
||||
*/
|
||||
public removeSection(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.sectionRef.removeSection(this.submissionId, this.sectionData.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the correct component based on the section's type
|
||||
*/
|
||||
getSectionContent(): string {
|
||||
return rendersSectionType(this.sectionData.sectionType);
|
||||
}
|
||||
|
@@ -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', () => {
|
||||
@@ -164,7 +164,7 @@ describe('SectionFormOperationsService test suite', () => {
|
||||
parent: null
|
||||
};
|
||||
|
||||
expect(service.isPartOfArrayOfGroup(model)).toBeFalsy();
|
||||
expect(service.isPartOfArrayOfGroup(model as any)).toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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 { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
|
||||
|
||||
/**
|
||||
* The service handling all form section operations
|
||||
*/
|
||||
@Injectable()
|
||||
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,
|
||||
event: DynamicFormControlEvent,
|
||||
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 {
|
||||
let fieldIndex: number;
|
||||
if (isNotEmpty(event)) {
|
||||
@@ -60,7 +91,15 @@ export class SectionFormOperationsService {
|
||||
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)
|
||||
&& (model.parent as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP
|
||||
&& (model.parent as any).parent
|
||||
@@ -68,7 +107,15 @@ export class SectionFormOperationsService {
|
||||
&& (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 context = this.formBuilder.isQualdropGroup(event.model)
|
||||
@@ -87,12 +134,28 @@ export class SectionFormOperationsService {
|
||||
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 {
|
||||
const fieldIndex = this.getArrayIndexFromEvent(event);
|
||||
const fieldId = this.getFieldPathSegmentedFromChangeEvent(event);
|
||||
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 {
|
||||
const fieldIndex = this.getArrayIndexFromEvent(event);
|
||||
const metadataValueMap = new Map();
|
||||
@@ -117,6 +180,14 @@ export class SectionFormOperationsService {
|
||||
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 {
|
||||
let fieldId;
|
||||
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
|
||||
@@ -129,6 +200,14 @@ export class SectionFormOperationsService {
|
||||
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 {
|
||||
let fieldValue;
|
||||
const value = (event.model as any).value;
|
||||
@@ -142,12 +221,12 @@ export class SectionFormOperationsService {
|
||||
if ((event.model as DsDynamicInputModel).hasAuthority) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((authority, index) => {
|
||||
authority = Object.assign(new AuthorityValue(), authority, {language});
|
||||
authority = Object.assign(new AuthorityValue(), authority, { language });
|
||||
value[index] = authority;
|
||||
});
|
||||
fieldValue = value;
|
||||
} else {
|
||||
fieldValue = Object.assign(new AuthorityValue(), value, {language});
|
||||
fieldValue = Object.assign(new AuthorityValue(), value, { language });
|
||||
}
|
||||
} else {
|
||||
// Language without Authority (input, textArea)
|
||||
@@ -162,6 +241,14 @@ export class SectionFormOperationsService {
|
||||
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> {
|
||||
const metadataValueMap = new Map();
|
||||
|
||||
@@ -177,6 +264,16 @@ export class SectionFormOperationsService {
|
||||
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,
|
||||
event: DynamicFormControlEvent,
|
||||
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,
|
||||
event: DynamicFormControlEvent,
|
||||
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>,
|
||||
pathCombiner: JsonPatchOperationPathCombiner,
|
||||
event: DynamicFormControlEvent,
|
||||
|
@@ -12,7 +12,7 @@ import { SubmissionServiceStub } from '../../../shared/testing/submission-servic
|
||||
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
|
||||
import { SectionsService } from '../sections.service';
|
||||
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 { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
|
||||
import { getMockFormOperationsService } from '../../../shared/mocks/mock-form-operations-service';
|
||||
@@ -132,11 +132,11 @@ const dynamicFormControlEvent: DynamicFormControlEvent = {
|
||||
type: DynamicFormControlEventType.Change
|
||||
};
|
||||
|
||||
describe('FormSectionComponent test suite', () => {
|
||||
describe('SubmissionSectionformComponent test suite', () => {
|
||||
|
||||
let comp: FormSectionComponent;
|
||||
let comp: SubmissionSectionformComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<FormSectionComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionformComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let sectionsServiceStub: SectionsServiceStub;
|
||||
let notificationsServiceStub: NotificationsServiceStub;
|
||||
@@ -163,7 +163,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
],
|
||||
declarations: [
|
||||
FormComponent,
|
||||
FormSectionComponent,
|
||||
SubmissionSectionformComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
@@ -180,7 +180,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
{ provide: 'sectionDataProvider', useValue: sectionObject },
|
||||
{ provide: 'submissionIdProvider', useValue: submissionId },
|
||||
ChangeDetectorRef,
|
||||
FormSectionComponent
|
||||
SubmissionSectionformComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -203,7 +203,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create FormSectionComponent', inject([FormSectionComponent], (app: FormSectionComponent) => {
|
||||
it('should create SubmissionSectionformComponent', inject([SubmissionSectionformComponent], (app: SubmissionSectionformComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FormSectionComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionformComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.get(SubmissionService);
|
||||
@@ -236,6 +236,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
|
||||
it('should init section properly', () => {
|
||||
const sectionData = {};
|
||||
formService.isValid.and.returnValue(observableOf(true));
|
||||
formConfigService.getConfigByHref.and.returnValue(observableOf(formConfigData));
|
||||
sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionData));
|
||||
spyOn(comp, 'initForm');
|
||||
@@ -265,6 +266,7 @@ describe('FormSectionComponent test suite', () => {
|
||||
|
||||
it('should set a section Error when init form model fails', () => {
|
||||
formBuilderService.modelFromConfiguration.and.throwError('test');
|
||||
translateService.instant.and.returnValue('test');
|
||||
const sectionData = {};
|
||||
const sectionError: SubmissionSectionError = {
|
||||
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', () => {
|
||||
spyOn(comp, 'initForm');
|
||||
spyOn(comp, 'checksForErrors');
|
||||
|
@@ -17,7 +17,6 @@ import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder
|
||||
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';
|
||||
import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer';
|
||||
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 { GlobalConfig } from '../../../../config/global-config.interface';
|
||||
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 { SectionsService } from '../sections.service';
|
||||
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({
|
||||
selector: 'ds-submission-section-form',
|
||||
styleUrls: ['./section-form.component.scss'],
|
||||
templateUrl: './section-form.component.html',
|
||||
})
|
||||
@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[];
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is updating
|
||||
* @type {boolean}
|
||||
*/
|
||||
public isUpdating = false;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is loading
|
||||
* @type {boolean}
|
||||
*/
|
||||
public isLoading = true;
|
||||
|
||||
/**
|
||||
* The form config
|
||||
* @type {SubmissionFormsModel}
|
||||
*/
|
||||
protected formConfig: SubmissionFormsModel;
|
||||
|
||||
/**
|
||||
* The form data
|
||||
* @type {any}
|
||||
*/
|
||||
protected formData: any = Object.create({});
|
||||
|
||||
/**
|
||||
* The [JsonPatchOperationPathCombiner] object
|
||||
* @type {JsonPatchOperationPathCombiner}
|
||||
*/
|
||||
protected pathCombiner: JsonPatchOperationPathCombiner;
|
||||
|
||||
/**
|
||||
* The [FormFieldPreviousValueObject] object
|
||||
* @type {FormFieldPreviousValueObject}
|
||||
*/
|
||||
protected previousValue: FormFieldPreviousValueObject = new FormFieldPreviousValueObject();
|
||||
|
||||
/**
|
||||
* The list of Subscription
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The FormComponent reference
|
||||
*/
|
||||
@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,
|
||||
protected formBuilderService: FormBuilderService,
|
||||
protected formOperationsService: SectionFormOperationsService,
|
||||
@@ -66,6 +132,9 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables and retrieve form configuration
|
||||
*/
|
||||
onSectionInit() {
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
|
||||
this.formId = this.formService.getUniqueId(this.sectionData.id);
|
||||
@@ -75,7 +144,7 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
tap((config: SubmissionFormsModel) => this.formConfig = config),
|
||||
flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)),
|
||||
take(1))
|
||||
.subscribe((sectionData: WorkspaceitemSectionDataType) => {
|
||||
.subscribe((sectionData: WorkspaceitemSectionFormObject) => {
|
||||
if (isUndefined(this.formModel)) {
|
||||
this.sectionData.errors = [];
|
||||
// Is the first loading so init form
|
||||
@@ -88,17 +157,32 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
onSectionDestroy() {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section status
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* the section status
|
||||
*/
|
||||
protected getSectionStatus(): Observable<boolean> {
|
||||
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 = [];
|
||||
|
||||
// compare current form data state with section data retrieved from store
|
||||
@@ -116,7 +200,13 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
return isNotEmpty(diffResult);
|
||||
}
|
||||
|
||||
initForm(sectionData: WorkspaceitemSectionDataType) {
|
||||
/**
|
||||
* Initialize form model
|
||||
*
|
||||
* @param sectionData
|
||||
* the section data retrieved from the server
|
||||
*/
|
||||
initForm(sectionData: WorkspaceitemSectionFormObject): void {
|
||||
try {
|
||||
this.formModel = this.formBuilderService.modelFromConfiguration(
|
||||
this.formConfig,
|
||||
@@ -124,28 +214,32 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
sectionData,
|
||||
this.submissionService.getSubmissionScope());
|
||||
} catch (e) {
|
||||
this.translate.get('error.submission.sections.init-form-error')
|
||||
.subscribe((msg) => {
|
||||
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
|
||||
const sectionError: SubmissionSectionError = {
|
||||
message: msg + e.toString(),
|
||||
message: msg,
|
||||
path: '/sections/' + this.sectionData.id
|
||||
};
|
||||
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)) {
|
||||
this.sectionData.data = sectionData;
|
||||
if (this.hasMetadataEnrichment(sectionData)) {
|
||||
this.translate.get('submission.sections.general.metadata-extracted', { sectionId: this.sectionData.id })
|
||||
.pipe(take(1))
|
||||
.subscribe((m) => {
|
||||
this.notificationsService.info(null, m, null, true);
|
||||
});
|
||||
const msg = this.translate.instant(
|
||||
'submission.sections.general.metadata-extracted',
|
||||
{ sectionId: this.sectionData.id });
|
||||
this.notificationsService.info(null, msg, null, true);
|
||||
this.isUpdating = true;
|
||||
this.formModel = null;
|
||||
this.cdr.detectChanges();
|
||||
@@ -153,6 +247,8 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
this.checksForErrors(errors);
|
||||
this.isUpdating = false;
|
||||
this.cdr.detectChanges();
|
||||
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
|
||||
this.checksForErrors(errors);
|
||||
}
|
||||
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.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(
|
||||
find((status: boolean) => status === true && !this.isUpdating))
|
||||
.subscribe(() => {
|
||||
@@ -170,9 +272,11 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
});
|
||||
}
|
||||
|
||||
subscriptions() {
|
||||
/**
|
||||
* Initialize all subscriptions
|
||||
*/
|
||||
subscriptions(): void {
|
||||
this.subs.push(
|
||||
|
||||
/**
|
||||
* Subscribe to form's data
|
||||
*/
|
||||
@@ -181,6 +285,7 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
.subscribe((formData) => {
|
||||
this.formData = formData;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Subscribe to section state
|
||||
*/
|
||||
@@ -190,12 +295,19 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
}),
|
||||
distinctUntilChanged())
|
||||
.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.pathCombiner,
|
||||
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 path = this.formBuilderService.getPath(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.pathCombiner,
|
||||
event,
|
||||
@@ -229,7 +355,15 @@ export class FormSectionComponent extends SectionModelComponent {
|
||||
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)) {
|
||||
return this.sectionData.data.hasOwnProperty(fieldId) && isNotEmpty(this.sectionData.data[fieldId][index]);
|
||||
} else {
|
||||
|
@@ -29,7 +29,7 @@ import {
|
||||
} from '../../../shared/mocks/mock-submission';
|
||||
import { FormComponent } from '../../../shared/form/form.component';
|
||||
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 { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
import { SectionFormOperationsService } from '../form/section-form-operations.service';
|
||||
@@ -78,11 +78,11 @@ const dynamicFormControlEvent: DynamicFormControlEvent = {
|
||||
type: DynamicFormControlEventType.Change
|
||||
};
|
||||
|
||||
describe('LicenseSectionComponent test suite', () => {
|
||||
describe('SubmissionSectionLicenseComponent test suite', () => {
|
||||
|
||||
let comp: LicenseSectionComponent;
|
||||
let comp: SubmissionSectionLicenseComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<LicenseSectionComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionLicenseComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let sectionsServiceStub: SectionsServiceStub;
|
||||
let formService: any;
|
||||
@@ -124,7 +124,7 @@ describe('LicenseSectionComponent test suite', () => {
|
||||
],
|
||||
declarations: [
|
||||
FormComponent,
|
||||
LicenseSectionComponent,
|
||||
SubmissionSectionLicenseComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
@@ -141,7 +141,7 @@ describe('LicenseSectionComponent test suite', () => {
|
||||
{ provide: 'submissionIdProvider', useValue: submissionId },
|
||||
ChangeDetectorRef,
|
||||
FormBuilderService,
|
||||
LicenseSectionComponent
|
||||
SubmissionSectionLicenseComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -164,7 +164,7 @@ describe('LicenseSectionComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create LicenseSectionComponent', inject([LicenseSectionComponent], (app: LicenseSectionComponent) => {
|
||||
it('should create SubmissionSectionLicenseComponent', inject([SubmissionSectionLicenseComponent], (app: SubmissionSectionLicenseComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('LicenseSectionComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LicenseSectionComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionLicenseComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.get(SubmissionService);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
|
||||
|
||||
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 {
|
||||
DynamicCheckboxModel,
|
||||
DynamicFormControlEvent,
|
||||
@@ -29,25 +29,79 @@ import { SectionsService } from '../sections.service';
|
||||
import { SectionFormOperationsService } from '../form/section-form-operations.service';
|
||||
import { FormComponent } from '../../../shared/form/form.component';
|
||||
|
||||
/**
|
||||
* This component represents a section that contains the submission license form.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-section-license',
|
||||
styleUrls: ['./section-license.component.scss'],
|
||||
templateUrl: './section-license.component.html',
|
||||
})
|
||||
@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[];
|
||||
|
||||
/**
|
||||
* The [[DynamicFormLayout]] object
|
||||
* @type {DynamicFormLayout}
|
||||
*/
|
||||
public formLayout: DynamicFormLayout = SECTION_LICENSE_FORM_LAYOUT;
|
||||
|
||||
/**
|
||||
* A boolean representing if to show form submit and cancel buttons
|
||||
* @type {boolean}
|
||||
*/
|
||||
public displaySubmit = false;
|
||||
|
||||
/**
|
||||
* The submission license text
|
||||
* @type {Array}
|
||||
*/
|
||||
public licenseText$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The [[JsonPatchOperationPathCombiner]] object
|
||||
* @type {JsonPatchOperationPathCombiner}
|
||||
*/
|
||||
protected pathCombiner: JsonPatchOperationPathCombiner;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The FormComponent reference
|
||||
*/
|
||||
@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,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected formBuilderService: FormBuilderService,
|
||||
@@ -62,6 +116,9 @@ export class LicenseSectionComponent extends SectionModelComponent {
|
||||
super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables and retrieve submission license
|
||||
*/
|
||||
onSectionInit() {
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 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> {
|
||||
const model = this.formBuilderService.findById('granted', this.formModel);
|
||||
return (model as DynamicCheckboxModel).valueUpdates.pipe(
|
||||
@@ -133,6 +196,10 @@ export class LicenseSectionComponent extends SectionModelComponent {
|
||||
startWith((model as DynamicCheckboxModel).value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when a form dfChange event is fired.
|
||||
* Dispatch form operations based on changes.
|
||||
*/
|
||||
onChange(event: DynamicFormControlEvent) {
|
||||
const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
|
||||
const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
|
||||
@@ -145,6 +212,9 @@ export class LicenseSectionComponent extends SectionModelComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
onSectionDestroy() {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
|
@@ -2,14 +2,48 @@ import { SubmissionSectionError } from '../../objects/submission-objects.reducer
|
||||
import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model';
|
||||
import { SectionsType } from '../sections-type';
|
||||
|
||||
/**
|
||||
* An interface to represent section model
|
||||
*/
|
||||
export interface SectionDataObject {
|
||||
|
||||
/**
|
||||
* The section configuration url
|
||||
*/
|
||||
config: string;
|
||||
|
||||
/**
|
||||
* The section data object
|
||||
*/
|
||||
data: WorkspaceitemSectionDataType;
|
||||
|
||||
/**
|
||||
* The list of the section errors
|
||||
*/
|
||||
errors: SubmissionSectionError[];
|
||||
|
||||
/**
|
||||
* The section header
|
||||
*/
|
||||
header: string;
|
||||
|
||||
/**
|
||||
* The section id
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is mandatory
|
||||
*/
|
||||
mandatory: boolean;
|
||||
|
||||
/**
|
||||
* The section type
|
||||
*/
|
||||
sectionType: SectionsType;
|
||||
|
||||
/**
|
||||
* Eventually additional fields
|
||||
*/
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
@@ -16,12 +16,44 @@ export interface SectionDataModel {
|
||||
*/
|
||||
export abstract class SectionModelComponent implements OnDestroy, OnInit, SectionDataModel {
|
||||
protected abstract sectionService: SectionsService;
|
||||
|
||||
/**
|
||||
* The collection id this submission belonging to
|
||||
* @type {string}
|
||||
*/
|
||||
collectionId: string;
|
||||
|
||||
/**
|
||||
* The section data
|
||||
* @type {SectionDataObject}
|
||||
*/
|
||||
sectionData: SectionDataObject;
|
||||
|
||||
/**
|
||||
* The submission id
|
||||
* @type {string}
|
||||
*/
|
||||
submissionId: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if this section is valid
|
||||
* @type {boolean}
|
||||
*/
|
||||
protected valid: boolean;
|
||||
|
||||
/**
|
||||
* The Subscription to section status observable
|
||||
* @type {Subscription}
|
||||
*/
|
||||
private sectionStatusSub: Subscription;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {string} injectedCollectionId
|
||||
* @param {SectionDataObject} injectedSectionData
|
||||
* @param {string} injectedSubmissionId
|
||||
*/
|
||||
public constructor(@Inject('collectionIdProvider') public injectedCollectionId: string,
|
||||
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
|
||||
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
|
||||
@@ -30,15 +62,43 @@ export abstract class SectionModelComponent implements OnDestroy, OnInit, Sectio
|
||||
this.submissionId = injectedSubmissionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call abstract methods on component init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.onSectionInit();
|
||||
this.updateSectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to implement to get section status
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* the section status
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Subscribe to section status
|
||||
*/
|
||||
protected updateSectionStatus(): void {
|
||||
this.sectionStatusSub = this.getSectionStatus().pipe(
|
||||
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 {
|
||||
if (hasValue(this.sectionStatusSub)) {
|
||||
this.sectionStatusSub.unsubscribe();
|
||||
|
@@ -10,28 +10,90 @@ import { SubmissionSectionError, SubmissionSectionObject } from '../objects/subm
|
||||
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
|
||||
import { SubmissionService } from '../submission.service';
|
||||
|
||||
/**
|
||||
* Directive for handling generic section functionality
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[dsSection]',
|
||||
exportAs: 'sectionRef'
|
||||
})
|
||||
export class SectionsDirective implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* A boolean representing if section is mandatory
|
||||
* @type {boolean}
|
||||
*/
|
||||
@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[] = [];
|
||||
|
||||
/**
|
||||
* The list of all errors related to the element belonging to this section
|
||||
* @type {Array}
|
||||
*/
|
||||
public allSectionErrors: string[] = [];
|
||||
|
||||
/**
|
||||
* A boolean representing if section is active
|
||||
* @type {boolean}
|
||||
*/
|
||||
private active = true;
|
||||
private animation = !this.mandatory;
|
||||
|
||||
/**
|
||||
* A boolean representing if section is enabled
|
||||
* @type {boolean}
|
||||
*/
|
||||
private enabled: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* A boolean representing the panel collapsible state: opened (true) or closed (false)
|
||||
* @type {boolean}
|
||||
*/
|
||||
private sectionState = this.mandatory;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* A boolean representing if section is valid
|
||||
* @type {boolean}
|
||||
*/
|
||||
private valid: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {ChangeDetectorRef} changeDetectorRef
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {SectionsService} sectionService
|
||||
*/
|
||||
constructor(private changeDetectorRef: ChangeDetectorRef,
|
||||
private submissionService: SubmissionService,
|
||||
private sectionService: SectionsService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.valid = this.sectionService.isSectionValid(this.submissionId, this.sectionId).pipe(
|
||||
map((valid: boolean) => {
|
||||
@@ -78,67 +140,145 @@ export class SectionsDirective implements OnDestroy, OnInit {
|
||||
this.enabled = this.sectionService.isSectionEnabled(this.submissionId, this.sectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Change section state
|
||||
*
|
||||
* @param event
|
||||
* the event emitted
|
||||
*/
|
||||
public sectionChange(event) {
|
||||
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;
|
||||
}
|
||||
|
||||
public isMandatory() {
|
||||
/**
|
||||
* Check if section is mandatory
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Returns true when section is mandatory
|
||||
*/
|
||||
public isMandatory(): boolean {
|
||||
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 {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if section is enabled
|
||||
*
|
||||
* @returns {Observable<boolean>}
|
||||
* Emits true whenever section is enabled
|
||||
*/
|
||||
public isEnabled(): Observable<boolean> {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if section is valid
|
||||
*
|
||||
* @returns {Observable<boolean>}
|
||||
* Emits true whenever section is valid
|
||||
*/
|
||||
public isValid(): Observable<boolean> {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) ||
|
||||
(this.allSectionErrors && this.allSectionErrors.length > 0)
|
||||
}
|
||||
|
||||
public getErrors() {
|
||||
/**
|
||||
* Return section errors
|
||||
*
|
||||
* @returns {Array}
|
||||
* Returns section errors list
|
||||
*/
|
||||
public getErrors(): string[] {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all errors from list
|
||||
*/
|
||||
public resetErrors() {
|
||||
if (isNotEmpty(this.genericSectionErrors)) {
|
||||
this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionId);
|
||||
|
@@ -35,9 +35,20 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { SubmissionService } from '../submission.service';
|
||||
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
|
||||
|
||||
/**
|
||||
* A service that provides methods used in submission process.
|
||||
*/
|
||||
@Injectable()
|
||||
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,
|
||||
private scrollToService: ScrollToService,
|
||||
private submissionService: SubmissionService,
|
||||
@@ -45,17 +56,35 @@ export class SectionsService {
|
||||
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(
|
||||
submissionId: string,
|
||||
sectionId: string,
|
||||
formId: string,
|
||||
currentErrors: SubmissionSectionError[],
|
||||
prevErrors: SubmissionSectionError[] = []) {
|
||||
// Remove previous error list if the current is empty
|
||||
if (isEmpty(currentErrors)) {
|
||||
this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
|
||||
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 = [];
|
||||
|
||||
// Itereate over the current error list
|
||||
currentErrors.forEach((error: SubmissionSectionError) => {
|
||||
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);
|
||||
|
||||
@@ -63,7 +92,7 @@ export class SectionsService {
|
||||
if (path.fieldId) {
|
||||
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);
|
||||
this.store.dispatch(formAddErrorAction);
|
||||
dispatchedErrors.push(fieldId);
|
||||
@@ -71,6 +100,7 @@ export class SectionsService {
|
||||
});
|
||||
});
|
||||
|
||||
// Itereate over the previous error list
|
||||
prevErrors.forEach((error: SubmissionSectionError) => {
|
||||
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);
|
||||
|
||||
@@ -79,6 +109,7 @@ export class SectionsService {
|
||||
const fieldId = path.fieldId.replace(/\./g, '_');
|
||||
|
||||
if (!dispatchedErrors.includes(fieldId)) {
|
||||
// Dispatch action to remove form error from the state;
|
||||
const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex);
|
||||
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) {
|
||||
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> {
|
||||
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
|
||||
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[]> {
|
||||
return this.store.select(submissionSectionErrorsFromIdSelector(submissionId, sectionId)).pipe(
|
||||
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> {
|
||||
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
|
||||
filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)),
|
||||
@@ -109,6 +178,16 @@ export class SectionsService {
|
||||
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> {
|
||||
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
|
||||
filter((sectionObj) => hasValue(sectionObj)),
|
||||
@@ -116,12 +195,32 @@ export class SectionsService {
|
||||
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> {
|
||||
return this.submissionService.getActiveSectionId(submissionId).pipe(
|
||||
map((activeSectionId: string) => sectionId === activeSectionId),
|
||||
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> {
|
||||
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
|
||||
filter((sectionObj) => hasValue(sectionObj)),
|
||||
@@ -129,6 +228,18 @@ export class SectionsService {
|
||||
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> {
|
||||
return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
|
||||
filter((sectionObj) => hasValue(sectionObj)),
|
||||
@@ -140,6 +251,16 @@ export class SectionsService {
|
||||
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> {
|
||||
return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
|
||||
filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
|
||||
@@ -149,8 +270,15 @@ export class SectionsService {
|
||||
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));
|
||||
const config: ScrollToConfigOptions = {
|
||||
target: sectionId,
|
||||
@@ -160,11 +288,31 @@ export class SectionsService {
|
||||
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) {
|
||||
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)) {
|
||||
const isAvailable$ = this.isSectionAvailable(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) {
|
||||
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) {
|
||||
this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status));
|
||||
}
|
||||
|
@@ -8,18 +8,37 @@ import { isEmpty } from '../../../../shared/empty.util';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
|
||||
/**
|
||||
* This component represents a badge that describe an access condition
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-access-conditions',
|
||||
templateUrl: './accessConditions.component.html',
|
||||
selector: 'ds-submission-section-upload-access-conditions',
|
||||
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[];
|
||||
|
||||
/**
|
||||
* The list of access conditions
|
||||
* @type {Array}
|
||||
*/
|
||||
public accessConditionsList = [];
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {GroupEpersonService} groupService
|
||||
*/
|
||||
constructor(private groupService: GroupEpersonService) {}
|
||||
|
||||
/**
|
||||
* Retrieve access conditions list
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.accessConditions.forEach((accessCondition: ResourcePolicy) => {
|
||||
if (isEmpty(accessCondition.name)) {
|
@@ -15,7 +15,7 @@ import { createTestComponent } from '../../../../../shared/testing/utils';
|
||||
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
|
||||
import { SubmissionServiceStub } from '../../../../../shared/testing/submission-service-stub';
|
||||
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 {
|
||||
mockGroup,
|
||||
@@ -30,12 +30,13 @@ import { FormService } from '../../../../../shared/form/form.service';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../config';
|
||||
import { MOCK_SUBMISSION_CONFIG } from '../../../../../shared/testing/mock-submission-config';
|
||||
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 fixture: ComponentFixture<UploadSectionFileEditComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionUploadFileEditComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let formbuilderService: any;
|
||||
|
||||
@@ -44,7 +45,10 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
const sectionId = 'upload';
|
||||
const collectionId = mockSubmissionCollectionId;
|
||||
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 configMetadataForm: any = mockUploadConfigResponse.metadata;
|
||||
const fileIndex = '0';
|
||||
@@ -62,7 +66,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
],
|
||||
declarations: [
|
||||
FormComponent,
|
||||
UploadSectionFileEditComponent,
|
||||
SubmissionSectionUploadFileEditComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
@@ -71,7 +75,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
FormBuilderService,
|
||||
ChangeDetectorRef,
|
||||
UploadSectionFileEditComponent
|
||||
SubmissionSectionUploadFileEditComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -84,7 +88,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `
|
||||
<ds-submission-upload-section-file-edit [availableAccessConditionGroups]="availableAccessConditionGroups"
|
||||
<ds-submission-section-upload-file-edit [availableAccessConditionGroups]="availableAccessConditionGroups"
|
||||
[availableAccessConditionOptions]="availableAccessConditionOptions"
|
||||
[collectionId]="collectionId"
|
||||
[collectionPolicyType]="collectionPolicyType"
|
||||
@@ -93,7 +97,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
[fileId]="fileId"
|
||||
[fileIndex]="fileIndex"
|
||||
[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>;
|
||||
testComp = testFixture.componentInstance;
|
||||
@@ -103,7 +107,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create UploadSectionFileEditComponent', inject([UploadSectionFileEditComponent], (app: UploadSectionFileEditComponent) => {
|
||||
it('should create SubmissionSectionUploadFileEditComponent', inject([SubmissionSectionUploadFileEditComponent], (app: SubmissionSectionUploadFileEditComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -112,7 +116,7 @@ describe('UploadSectionFileEditComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UploadSectionFileEditComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionUploadFileEditComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.get(SubmissionService);
|
@@ -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 {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
|
||||
DynamicDateControlModel,
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
DynamicFormGroupModel,
|
||||
DynamicSelectModel
|
||||
} 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 {
|
||||
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
|
||||
@@ -26,39 +28,122 @@ import {
|
||||
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT,
|
||||
BITSTREAM_METADATA_FORM_GROUP_CONFIG,
|
||||
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 { isNotEmpty, isNotUndefined } from '../../../../../shared/empty.util';
|
||||
import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model';
|
||||
import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model';
|
||||
import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model';
|
||||
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({
|
||||
selector: 'ds-submission-upload-section-file-edit',
|
||||
templateUrl: './file-edit.component.html',
|
||||
selector: 'ds-submission-section-upload-file-edit',
|
||||
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() 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[];
|
||||
|
||||
/**
|
||||
* 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,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private formService: FormService,
|
||||
private submissionService: SubmissionService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch form model init
|
||||
*/
|
||||
ngOnChanges() {
|
||||
if (this.fileData && this.formId) {
|
||||
this.formModel = this.buildFileEditForm();
|
||||
@@ -66,8 +151,10 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form model
|
||||
*/
|
||||
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]);
|
||||
configDescr.repeatable = false;
|
||||
const configForm = Object.assign({}, this.configMetadataForm, {
|
||||
@@ -100,7 +187,7 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
}
|
||||
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 hasEnd = [];
|
||||
const hasGroups = [];
|
||||
@@ -146,6 +233,12 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
return formModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form model values
|
||||
*
|
||||
* @param formModel
|
||||
* The form model
|
||||
*/
|
||||
public initModelData(formModel: DynamicFormControlModel[]) {
|
||||
this.fileData.accessConditions.forEach((accessCondition, index) => {
|
||||
Array.of('name', 'groupUUID', 'startDate', 'endDate')
|
||||
@@ -153,8 +246,8 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
.forEach((key) => {
|
||||
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
|
||||
if (metadataModel) {
|
||||
if (key === 'groupUUID') {
|
||||
this.availableAccessConditionGroups.forEach((group) => {
|
||||
if (key === 'groupUUID' && this.availableAccessConditionGroups.get(accessCondition.name)) {
|
||||
this.availableAccessConditionGroups.get(accessCondition.name).forEach((group) => {
|
||||
metadataModel.options.push({
|
||||
label: group.name,
|
||||
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) {
|
||||
if (event.model.id === 'name') {
|
||||
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;
|
||||
this.availableAccessConditionOptions.filter((element) => element.name === control.value)
|
||||
.forEach((element) => accessCondition = element);
|
||||
if (isNotEmpty(accessCondition)) {
|
||||
const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true;
|
||||
|
||||
const groupControl = control.parent.get('groupUUID');
|
||||
const startDateControl = control.parent.get('startDate');
|
||||
const endDateControl = control.parent.get('endDate');
|
||||
const groupControl: FormControl = control.parent.get('groupUUID') as FormControl;
|
||||
const startDateControl: FormControl = control.parent.get('startDate') as FormControl;
|
||||
const endDateControl: FormControl = control.parent.get('endDate') as FormControl;
|
||||
|
||||
// Clear previous state
|
||||
groupControl.markAsUntouched();
|
||||
startDateControl.markAsUntouched();
|
||||
endDateControl.markAsUntouched();
|
||||
|
||||
// Clear previous values
|
||||
if (showGroups) {
|
||||
groupControl.setValue(null);
|
||||
} else {
|
||||
groupControl.clearValidators();
|
||||
groupControl.setValue(accessCondition.groupUUID);
|
||||
}
|
||||
startDateControl.setValue(null);
|
||||
@@ -204,15 +317,15 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
endDateControl.setValue(null);
|
||||
|
||||
if (showGroups) {
|
||||
if (isNotUndefined(accessCondition.groupUUID)) {
|
||||
if (isNotUndefined(accessCondition.groupUUID) || isNotUndefined(accessCondition.selectGroupUUID)) {
|
||||
|
||||
const groupOptions = [];
|
||||
if (isNotUndefined(this.availableAccessConditionGroups.get(accessCondition.groupUUID))) {
|
||||
if (isNotUndefined(this.availableAccessConditionGroups.get(accessCondition.name))) {
|
||||
const groupModel = this.formBuilderService.findById(
|
||||
'groupUUID',
|
||||
(model.parent as DynamicFormArrayGroupModel).group) as DynamicSelectModel<any>;
|
||||
|
||||
this.availableAccessConditionGroups.forEach((group) => {
|
||||
this.availableAccessConditionGroups.get(accessCondition.name).forEach((group) => {
|
||||
groupOptions.push({
|
||||
label: group.name,
|
||||
value: group.uuid
|
||||
@@ -223,8 +336,8 @@ export class UploadSectionFileEditComponent implements OnChanges {
|
||||
const confGroup = { relation: groupModel.relation };
|
||||
const groupsConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, confGroup);
|
||||
groupsConfig.options = groupOptions;
|
||||
model.parent.group.pop();
|
||||
model.parent.group.push(new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT));
|
||||
(model.parent as DynamicFormGroupModel).group.pop();
|
||||
(model.parent as DynamicFormGroupModel).group.push(new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT));
|
||||
}
|
||||
|
||||
}
|
@@ -56,7 +56,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke
|
||||
connective: 'OR',
|
||||
when: []
|
||||
}
|
||||
]
|
||||
],
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'submission.sections.upload.form.date-required'
|
||||
}
|
||||
};
|
||||
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
|
||||
element: {
|
||||
@@ -80,7 +87,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM
|
||||
connective: 'OR',
|
||||
when: []
|
||||
}
|
||||
]
|
||||
],
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'submission.sections.upload.form.date-required'
|
||||
}
|
||||
};
|
||||
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
|
||||
element: {
|
||||
@@ -102,7 +116,14 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG: DynamicSelectModelCo
|
||||
connective: 'OR',
|
||||
when: []
|
||||
}
|
||||
]
|
||||
],
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'submission.sections.upload.form.group-required'
|
||||
}
|
||||
};
|
||||
export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT: DynamicFormControlLayout = {
|
||||
element: {
|
@@ -34,9 +34,9 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<ds-submission-upload-section-file-view *ngIf="readMode"
|
||||
[fileData]="fileData"></ds-submission-upload-section-file-view>
|
||||
<ds-submission-upload-section-file-edit *ngIf="!readMode"
|
||||
<ds-submission-section-upload-file-view *ngIf="readMode"
|
||||
[fileData]="fileData"></ds-submission-section-upload-file-view>
|
||||
<ds-submission-section-upload-file-edit *ngIf="!readMode"
|
||||
[availableAccessConditionGroups]="availableAccessConditionGroups"
|
||||
[availableAccessConditionOptions]="availableAccessConditionOptions"
|
||||
[collectionId]="collectionId"
|
||||
@@ -46,7 +46,7 @@
|
||||
[fileId]="fileId"
|
||||
[fileIndex]="fileIndex"
|
||||
[formId]="formId"
|
||||
[sectionId]="sectionId"></ds-submission-upload-section-file-edit>
|
||||
[sectionId]="sectionId"></ds-submission-section-upload-file-edit>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
@@ -15,7 +15,7 @@ import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
import { SubmissionJsonPatchOperationsServiceStub } from '../../../../shared/testing/submission-json-patch-operations-service-stub';
|
||||
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 {
|
||||
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 { getMockSectionUploadService } from '../../../../shared/mocks/mock-section-upload.service';
|
||||
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 {
|
||||
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 fixture: ComponentFixture<UploadSectionFileComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionUploadFileComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let uploadService: any;
|
||||
let fileService: any;
|
||||
@@ -61,7 +63,10 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
const sectionId = 'upload';
|
||||
const collectionId = mockSubmissionCollectionId;
|
||||
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 fileIndex = '0';
|
||||
const fileName = '123456-test-upload.jpg';
|
||||
@@ -85,7 +90,7 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
],
|
||||
declarations: [
|
||||
FileSizePipe,
|
||||
UploadSectionFileComponent,
|
||||
SubmissionSectionUploadFileComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
@@ -98,7 +103,8 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
{ provide: SectionUploadService, useValue: getMockSectionUploadService() },
|
||||
ChangeDetectorRef,
|
||||
NgbModal,
|
||||
UploadSectionFileComponent
|
||||
SubmissionSectionUploadFileComponent,
|
||||
SubmissionSectionUploadFileEditComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -130,7 +136,7 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create UploadSectionFileComponent', inject([UploadSectionFileComponent], (app: UploadSectionFileComponent) => {
|
||||
it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -139,7 +145,7 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UploadSectionFileComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.get(SubmissionService);
|
||||
@@ -228,10 +234,14 @@ describe('UploadSectionFileComponent test suite', () => {
|
||||
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;
|
||||
const event = new Event('click', null);
|
||||
spyOn(comp, 'switchMode');
|
||||
formService.validateAllFormFields.and.callFake(() => null);
|
||||
formService.isValid.and.returnValue(observableOf(true));
|
||||
formService.getFormData.and.returnValue(observableOf(mockFileFormData));
|
||||
|
||||
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', () => {
|
||||
let field;
|
||||
expect(compAsAny.retrieveValueFromField(field)).toBeUndefined();
|
@@ -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 { filter, first, flatMap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { filter, first, flatMap, take } from 'rxjs/operators';
|
||||
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
|
||||
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 { SubmissionObject } from '../../../../core/submission/models/submission-object.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({
|
||||
selector: 'ds-submission-upload-section-file',
|
||||
styleUrls: ['./file.component.scss'],
|
||||
templateUrl: './file.component.html',
|
||||
styleUrls: ['./section-upload-file.component.scss'],
|
||||
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() 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 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[];
|
||||
|
||||
/**
|
||||
* A boolean representing if a submission delete operation is pending
|
||||
* @type {BehaviorSubject<boolean>}
|
||||
*/
|
||||
public processingDelete$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* The [JsonPatchOperationPathCombiner] object
|
||||
* @type {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,
|
||||
private fileService: FileService,
|
||||
private formService: FormService,
|
||||
@@ -60,6 +168,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
this.readMode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bitstream's metadata
|
||||
*/
|
||||
ngOnChanges() {
|
||||
if (this.availableAccessConditionOptions && this.availableAccessConditionGroups) {
|
||||
// Retrieve file state
|
||||
@@ -75,11 +186,17 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.formId = this.formService.getUniqueId(this.fileId);
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete bitstream from submission
|
||||
*/
|
||||
protected deleteFile() {
|
||||
this.operationsBuilder.remove(this.pathCombiner.getPath());
|
||||
this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
|
||||
@@ -93,6 +210,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show confirmation dialog for delete
|
||||
*/
|
||||
public confirmDelete(content) {
|
||||
this.modalService.open(content).result.then(
|
||||
(result) => {
|
||||
@@ -104,6 +224,9 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform bitstream download
|
||||
*/
|
||||
public downloadBitstreamFile() {
|
||||
this.halService.getEndpoint('bitstreams').pipe(
|
||||
first())
|
||||
@@ -113,12 +236,24 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save bitstream metadata
|
||||
*
|
||||
* @param event
|
||||
* the click event emitted
|
||||
*/
|
||||
public saveBitstreamData(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.subscriptions.push(this.formService.getFormData(this.formId).pipe(
|
||||
first(),
|
||||
// validate form
|
||||
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) => {
|
||||
// collect bitstream metadata
|
||||
Object.keys((formData.metadata))
|
||||
.filter((key) => isNotEmpty(formData.metadata[key]))
|
||||
.forEach((key) => {
|
||||
@@ -166,6 +301,7 @@ export class UploadSectionFileComponent implements OnChanges, OnInit {
|
||||
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
|
||||
}
|
||||
|
||||
// dispatch a PATCH request to save metadata
|
||||
return this.operationsService.jsonPatchByResourceID(
|
||||
this.submissionService.getSubmissionObjectLinkName(),
|
||||
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;
|
||||
return (temp) ? temp.value : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch from edit form to metadata view
|
||||
*/
|
||||
public switchMode() {
|
||||
this.readMode = !this.readMode;
|
||||
this.cdr.detectChanges();
|
@@ -25,5 +25,5 @@
|
||||
</ng-container>
|
||||
|
||||
<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>
|
@@ -6,15 +6,15 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { createTestComponent } from '../../../../../shared/testing/utils';
|
||||
import { mockUploadFiles } from '../../../../../shared/mocks/mock-submission';
|
||||
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 { Metadata } from '../../../../../core/shared/metadata.utils';
|
||||
|
||||
describe('UploadSectionFileViewComponent test suite', () => {
|
||||
describe('SubmissionSectionUploadFileViewComponent test suite', () => {
|
||||
|
||||
let comp: UploadSectionFileViewComponent;
|
||||
let comp: SubmissionSectionUploadFileViewComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<UploadSectionFileViewComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionUploadFileViewComponent>;
|
||||
|
||||
const fileData: any = mockUploadFiles[0];
|
||||
|
||||
@@ -26,11 +26,11 @@ describe('UploadSectionFileViewComponent test suite', () => {
|
||||
declarations: [
|
||||
TruncatePipe,
|
||||
FormComponent,
|
||||
UploadSectionFileViewComponent,
|
||||
SubmissionSectionUploadFileViewComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
UploadSectionFileViewComponent
|
||||
SubmissionSectionUploadFileViewComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -43,7 +43,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
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>;
|
||||
testComp = testFixture.componentInstance;
|
||||
@@ -53,7 +53,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create UploadSectionFileViewComponent', inject([UploadSectionFileViewComponent], (app: UploadSectionFileViewComponent) => {
|
||||
it('should create SubmissionSectionUploadFileViewComponent', inject([SubmissionSectionUploadFileViewComponent], (app: SubmissionSectionUploadFileViewComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('UploadSectionFileViewComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UploadSectionFileViewComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionUploadFileViewComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
});
|
@@ -5,17 +5,42 @@ import { isNotEmpty } from '../../../../../shared/empty.util';
|
||||
import { Metadata } from '../../../../../core/shared/metadata.utils';
|
||||
import { MetadataMap, MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
|
||||
/**
|
||||
* This component allow to show bitstream's metadata
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-upload-section-file-view',
|
||||
templateUrl: './file-view.component.html',
|
||||
selector: 'ds-submission-section-upload-file-view',
|
||||
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;
|
||||
|
||||
/**
|
||||
* The [[MetadataMap]] object
|
||||
* @type {MetadataMap}
|
||||
*/
|
||||
public metadata: MetadataMap = Object.create({});
|
||||
|
||||
/**
|
||||
* The bitstream's title key
|
||||
* @type {string}
|
||||
*/
|
||||
public fileTitleKey = 'Title';
|
||||
|
||||
/**
|
||||
* The bitstream's description key
|
||||
* @type {string}
|
||||
*/
|
||||
public fileDescrKey = 'Description';
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
if (isNotEmpty(this.fileData.metadata)) {
|
||||
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);
|
||||
}
|
||||
}
|
@@ -15,17 +15,14 @@
|
||||
<div *ngIf="collectionDefaultAccessConditions.length > 0" class="row">
|
||||
<div class="col-sm-12" >
|
||||
<ds-alert [type]="AlertTypeEnum.Warning">
|
||||
<!-- no def , no banner -->
|
||||
<ng-container *ngIf="collectionPolicyType === 1">
|
||||
<!-- def e no scelta -->
|
||||
{{ 'submission.sections.upload.header.policy.default.nolist' | translate:{ "collectionName": collectionName } }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="collectionPolicyType === 2">
|
||||
<!-- def e scelta -->
|
||||
{{ 'submission.sections.upload.header.policy.default.withlist' | translate:{ "collectionName": collectionName } }}
|
||||
</ng-container>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -24,7 +24,7 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.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 { GroupEpersonService } from '../../../core/eperson/group-eperson.service';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
@@ -71,11 +71,11 @@ const sectionObject: SectionDataObject = {
|
||||
sectionType: SectionsType.Upload
|
||||
};
|
||||
|
||||
describe('UploadSectionComponent test suite', () => {
|
||||
describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
|
||||
let comp: UploadSectionComponent;
|
||||
let comp: SubmissionSectionUploadComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<UploadSectionComponent>;
|
||||
let fixture: ComponentFixture<SubmissionSectionUploadComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let sectionsServiceStub: SectionsServiceStub;
|
||||
let collectionDataService: any;
|
||||
@@ -114,7 +114,7 @@ describe('UploadSectionComponent test suite', () => {
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
UploadSectionComponent,
|
||||
SubmissionSectionUploadComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
@@ -127,7 +127,7 @@ describe('UploadSectionComponent test suite', () => {
|
||||
{ provide: 'sectionDataProvider', useValue: sectionObject },
|
||||
{ provide: 'submissionIdProvider', useValue: submissionId },
|
||||
ChangeDetectorRef,
|
||||
UploadSectionComponent
|
||||
SubmissionSectionUploadComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
@@ -150,7 +150,7 @@ describe('UploadSectionComponent test suite', () => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create UploadSectionComponent', inject([UploadSectionComponent], (app: UploadSectionComponent) => {
|
||||
it('should create SubmissionSectionUploadComponent', inject([SubmissionSectionUploadComponent], (app: SubmissionSectionUploadComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('UploadSectionComponent test suite', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UploadSectionComponent);
|
||||
fixture = TestBed.createComponent(SubmissionSectionUploadComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
submissionServiceStub = TestBed.get(SubmissionService);
|
||||
@@ -204,15 +204,17 @@ describe('UploadSectionComponent test suite', () => {
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
const expectedGroupsMap = new Map();
|
||||
expectedGroupsMap.set(mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid });
|
||||
const expectedGroupsMap = new Map([
|
||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||
]);
|
||||
|
||||
expect(comp.collectionId).toBe(collectionId);
|
||||
expect(comp.collectionName).toBe(mockCollection.name);
|
||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||
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.fileList).toEqual([]);
|
||||
expect(compAsAny.fileIndexes).toEqual([]);
|
||||
@@ -248,15 +250,17 @@ describe('UploadSectionComponent test suite', () => {
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
const expectedGroupsMap = new Map();
|
||||
expectedGroupsMap.set(mockGroup.id, { name: mockGroup.name, uuid: mockGroup.uuid });
|
||||
const expectedGroupsMap = new Map([
|
||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||
]);
|
||||
|
||||
expect(comp.collectionId).toBe(collectionId);
|
||||
expect(comp.collectionName).toBe(mockCollection.name);
|
||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||
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.fileList).toEqual(mockUploadFiles);
|
||||
expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
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 { SectionModelComponent } from '../models/section.model';
|
||||
@@ -15,7 +15,7 @@ import { SectionsType } from '../sections-type';
|
||||
import { renderSectionFor } from '../sections-decorator';
|
||||
import { SectionDataObject } from '../models/section-data.model';
|
||||
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 { Group } from '../../../core/eperson/models/group.model';
|
||||
import { SectionsService } from '../sections.service';
|
||||
@@ -23,49 +23,105 @@ import { SubmissionService } from '../../submission.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ResourcePolicy } from '../../../core/shared/resource-policy.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_WITH_LIST = 2; // Banner2
|
||||
|
||||
export interface AccessConditionGroupsMapEntry {
|
||||
accessCondition: string;
|
||||
groups: Group[]
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represents a section that contains submission's bitstreams
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-submission-section-upload',
|
||||
styleUrls: ['./section-upload.component.scss'],
|
||||
templateUrl: './section-upload.component.html',
|
||||
})
|
||||
@renderSectionFor(SectionsType.Upload)
|
||||
export class UploadSectionComponent extends SectionModelComponent {
|
||||
export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {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;
|
||||
|
||||
/*
|
||||
/**
|
||||
* Default access conditions of this collection
|
||||
* @type {Array}
|
||||
*/
|
||||
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>;
|
||||
|
||||
/*
|
||||
/**
|
||||
* List of available access conditions that could be setted to files
|
||||
*/
|
||||
public availableAccessConditionOptions: AccessConditionOption[]; // List of accessConditions that an user can select
|
||||
|
||||
/*
|
||||
/**
|
||||
* 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,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private collectionDataService: CollectionDataService,
|
||||
@@ -78,10 +134,14 @@ export class UploadSectionComponent extends SectionModelComponent {
|
||||
super(undefined, injectedSectionData, injectedSubmissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables and retrieve collection default access conditions
|
||||
*/
|
||||
onSectionInit() {
|
||||
const config$ = this.uploadsConfigService.getConfigByHref(this.sectionData.config).pipe(
|
||||
map((config) => config.payload));
|
||||
|
||||
// retrieve configuration for the bitstream's metadata form
|
||||
this.configMetadataForm$ = config$.pipe(
|
||||
take(1),
|
||||
map((config: SubmissionUploadsModel) => config.metadata));
|
||||
@@ -117,41 +177,48 @@ export class UploadSectionComponent extends SectionModelComponent {
|
||||
: POLICY_DEFAULT_NO_LIST;
|
||||
|
||||
this.availableGroups = new Map();
|
||||
const groups$ = [];
|
||||
// Retrieve Groups for accessConditionPolicies
|
||||
const mapGroups$: Array<Observable<AccessConditionGroupsMapEntry>> = [];
|
||||
// Retrieve Groups for accessCondition Policies
|
||||
this.availableAccessConditionOptions.forEach((accessCondition: AccessConditionOption) => {
|
||||
if (accessCondition.hasEndDate === true || accessCondition.hasStartDate === true) {
|
||||
groups$.push(
|
||||
if (accessCondition.groupUUID) {
|
||||
mapGroups$.push(
|
||||
this.groupService.findById(accessCondition.groupUUID).pipe(
|
||||
find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded))
|
||||
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),
|
||||
reduce((acc: Group[], group: RemoteData<Group>) => {
|
||||
acc.push(group.payload);
|
||||
flatMap((entry) => entry),
|
||||
reduce((acc: any[], entry: AccessConditionGroupsMapEntry) => {
|
||||
acc.push(entry);
|
||||
return acc;
|
||||
}, []),
|
||||
).subscribe((groups: Group[]) => {
|
||||
groups.forEach((group: Group) => {
|
||||
if (isUndefined(this.availableGroups.get(group.uuid))) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
).subscribe((entries: AccessConditionGroupsMapEntry[]) => {
|
||||
entries.forEach((entry: AccessConditionGroupsMapEntry) => {
|
||||
this.availableGroups.set(entry.accessCondition, entry.groups);
|
||||
});
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
,
|
||||
}),
|
||||
|
||||
// retrieve submission's bitstreams from state
|
||||
combineLatest(this.configMetadataForm$,
|
||||
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
|
||||
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
|
||||
@@ -177,6 +244,14 @@ export class UploadSectionComponent extends SectionModelComponent {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return file name from metadata
|
||||
*
|
||||
* @param configMetadataForm
|
||||
* the bitstream's form configuration
|
||||
* @param fileData
|
||||
* the file metadata
|
||||
*/
|
||||
private getFileName(configMetadataForm: SubmissionFormsModel, fileData: any): string {
|
||||
const metadataName: string = configMetadataForm.rows[0].fields[0].selectableMetadata[0].metadata;
|
||||
let title: string;
|
||||
@@ -189,6 +264,12 @@ export class UploadSectionComponent extends SectionModelComponent {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section status
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* the section status
|
||||
*/
|
||||
protected getSectionStatus(): Observable<boolean> {
|
||||
return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe(
|
||||
map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0)));
|
||||
|
@@ -14,51 +14,127 @@ import { submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSe
|
||||
import { isUndefined } from '../../../shared/empty.util';
|
||||
import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model';
|
||||
|
||||
/**
|
||||
* A service that provides methods to handle submission's bitstream state.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SectionUploadService {
|
||||
|
||||
/**
|
||||
* Initialize service variables
|
||||
*
|
||||
* @param {Store<SubmissionState>} store
|
||||
*/
|
||||
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> {
|
||||
return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe(
|
||||
map((state) => state),
|
||||
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(
|
||||
filter((state) => !isUndefined(state)),
|
||||
map((state) => {
|
||||
let fileState;
|
||||
Object.keys(state)
|
||||
.filter((key) => state[key].uuid === fileUuid)
|
||||
.filter((key) => state[key].uuid === fileUUID)
|
||||
.forEach((key) => fileState = state[key]);
|
||||
return fileState;
|
||||
}),
|
||||
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),
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
new DeleteUploadedFileAction(submissionId, sectionId, fileId)
|
||||
new DeleteUploadedFileAction(submissionId, sectionId, fileUUID)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,9 @@ import { hasValue } from '../shared/empty.util';
|
||||
import { submissionSelector, SubmissionState } from './submission.reducers';
|
||||
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> {
|
||||
return createSelector(parentSelector, (state: T) => {
|
||||
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> {
|
||||
return createSelector(parentSelector, (state: T) => {
|
||||
if (hasValue(state) && hasValue(state[subState])) {
|
||||
|
@@ -6,21 +6,45 @@ import { SubmissionService } from './submission.service';
|
||||
import { SubmissionObject } from '../core/submission/models/submission-object.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
|
||||
/**
|
||||
* Instance of SubmissionService used on SSR.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerSubmissionService extends SubmissionService {
|
||||
|
||||
/**
|
||||
* Override createSubmission parent method to return an empty observable
|
||||
*
|
||||
* @return Observable<SubmissionObject>
|
||||
* observable of SubmissionObject
|
||||
*/
|
||||
createSubmission(): Observable<SubmissionObject> {
|
||||
return observableOf(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override retrieveSubmission parent method to return an empty observable
|
||||
*
|
||||
* @return Observable<SubmissionObject>
|
||||
* observable of SubmissionObject
|
||||
*/
|
||||
retrieveSubmission(submissionId): Observable<RemoteData<SubmissionObject>> {
|
||||
return observableOf(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override startAutoSave parent method and return without doing anything
|
||||
*
|
||||
* @param submissionId
|
||||
* The submission id
|
||||
*/
|
||||
startAutoSave(submissionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override startAutoSave parent method and return without doing anything
|
||||
*/
|
||||
stopAutoSave() {
|
||||
return;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user