diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 9183a3ec4d..96a0c54a47 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -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" } } diff --git a/src/app/+login-page/login-page.component.ts b/src/app/+login-page/login-page.component.ts index 1f1cf7cf04..6a8508eb45 100644 --- a/src/app/+login-page/login-page.component.ts +++ b/src/app/+login-page/login-page.component.ts @@ -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} store + */ constructor(private route: ActivatedRoute, private store: Store) {} + /** + * 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(); diff --git a/src/app/core/config/models/config-access-condition-option.model.ts b/src/app/core/config/models/config-access-condition-option.model.ts index 0c25d9aa0f..46bf1b60ce 100644 --- a/src/app/core/config/models/config-access-condition-option.model.ts +++ b/src/app/core/config/models/config-access-condition-option.model.ts @@ -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 */ diff --git a/src/app/core/config/models/config-object-factory.ts b/src/app/core/config/models/config-object-factory.ts index 5dbba7a11f..44b2e377c4 100644 --- a/src/app/core/config/models/config-object-factory.ts +++ b/src/app/core/config/models/config-object-factory.ts @@ -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 { switch (type) { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 648d02d4ca..f9e60c2968 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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'; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 815c531218..fc4da69a5c 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -53,6 +53,14 @@ export abstract class DataService { public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable + /** + * Create the HREF with given options object + * + * @param options The [[FindAllOptions]] object + * @param linkPath The link path for the object + * @return {Observable} + * Return an observable that emits created HREF + */ protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable { let result: Observable; const args = []; @@ -62,6 +70,14 @@ export abstract class DataService { 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} + * Return an observable that emits created HREF + */ protected getSearchByHref(searchMethod: string, options: FindAllOptions = {}): Observable { let result: Observable; const args = []; @@ -77,6 +93,15 @@ export abstract class DataService { 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} + * Return an observable that emits created HREF + */ protected buildHrefFromFindOptions(href$: Observable, args: string[], options: FindAllOptions): Observable { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { @@ -140,12 +165,25 @@ export abstract class DataService { return this.rdbService.buildSingle(href); } + /** + * Return object search endpoint by given search method + * + * @param searchMethod The search method for the object + */ protected getSearchEndpoint(searchMethod: string): Observable { 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>} + * Return an observable that emits response from the server + */ protected searchBy(searchMethod: string, options: FindAllOptions = {}): Observable>> { const hrefObs = this.getSearchByHref(searchMethod, options); diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index ae6e18fbf7..b83481fb24 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -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; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 1bf86dc1e2..6a88fdfdcb 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -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(request: RestRequest, forceBypassCache: boolean = false): void { const isGetRequest = request.method === RestRequestMethod.GET; if (forceBypassCache) { diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index 7d2138b633..32286929ee 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -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>>; + /** + * 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 */ diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 27fa0ef595..91ce5d90f3 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -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>>; + /** + * 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; } diff --git a/src/app/core/eperson/models/normalized-eperson.model.ts b/src/app/core/eperson/models/normalized-eperson.model.ts index 6bb66e93e6..ad4b20ee80 100644 --- a/src/app/core/eperson/models/normalized-eperson.model.ts +++ b/src/app/core/eperson/models/normalized-eperson.model.ts @@ -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 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; } diff --git a/src/app/core/eperson/models/normalized-group.model.ts b/src/app/core/eperson/models/normalized-group.model.ts index 8cfd24524c..f86bec8628 100644 --- a/src/app/core/eperson/models/normalized-group.model.ts +++ b/src/app/core/eperson/models/normalized-group.model.ts @@ -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 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; } diff --git a/src/app/core/json-patch/selectors.ts b/src/app/core/json-patch/selectors.ts index f6df4e1d07..1ccde294de 100644 --- a/src/app/core/json-patch/selectors.ts +++ b/src/app/core/json-patch/selectors.ts @@ -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(parentSelector: Selector, subState: string, key: string): MemoizedSelector { - return createSelector(parentSelector, (state: T) => { - if (hasValue(state[subState])) { - return state[subState][key]; - } else { - return undefined; - } - }); -} - -export function subStateSelector(parentSelector: Selector, subState: string): MemoizedSelector { - 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 diff --git a/src/app/core/submission/models/edititem.model.ts b/src/app/core/submission/models/edititem.model.ts deleted file mode 100644 index 9c8da2ab5a..0000000000 --- a/src/app/core/submission/models/edititem.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Workspaceitem } from './workspaceitem.model'; - -export class EditItem extends Workspaceitem { -} diff --git a/src/app/core/submission/models/normalized-edititem.model.ts b/src/app/core/submission/models/normalized-edititem.model.ts deleted file mode 100644 index 5615512399..0000000000 --- a/src/app/core/submission/models/normalized-edititem.model.ts +++ /dev/null @@ -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 { - -} diff --git a/src/app/core/submission/models/normalized-workflowitem.model.ts b/src/app/core/submission/models/normalized-workflowitem.model.ts index 0ea4ff6150..a3fa8992a2 100644 --- a/src/app/core/submission/models/normalized-workflowitem.model.ts +++ b/src/app/core/submission/models/normalized-workflowitem.model.ts @@ -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 { + /** + * 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; diff --git a/src/app/core/submission/models/normalized-workspaceitem.model.ts b/src/app/core/submission/models/normalized-workspaceitem.model.ts index 7ec40d6524..7c15925c98 100644 --- a/src/app/core/submission/models/normalized-workspaceitem.model.ts +++ b/src/app/core/submission/models/normalized-workspaceitem.model.ts @@ -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 { + /** + * 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; diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts index 7e3b74a6a9..6b2d9a03b9 100644 --- a/src/app/core/submission/models/submission-object.model.ts +++ b/src/app/core/submission/models/submission-object.model.ts @@ -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> | SubmissionDefinitionsModel; diff --git a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts index ca2f21de47..8b89397f24 100644 --- a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts +++ b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts @@ -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; } diff --git a/src/app/core/submission/models/workflowitem.model.ts b/src/app/core/submission/models/workflowitem.model.ts index 3df49c91f7..f1a0467f43 100644 --- a/src/app/core/submission/models/workflowitem.model.ts +++ b/src/app/core/submission/models/workflowitem.model.ts @@ -1,4 +1,7 @@ import { Workspaceitem } from './workspaceitem.model'; +/** + * A model class for a Workflowitem. + */ export class Workflowitem extends Workspaceitem { } diff --git a/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts b/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts deleted file mode 100644 index 9233780be8..0000000000 --- a/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts +++ /dev/null @@ -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; -} diff --git a/src/app/core/submission/models/workspaceitem-section-form.model.ts b/src/app/core/submission/models/workspaceitem-section-form.model.ts index cfae3f5b0f..1462a96d81 100644 --- a/src/app/core/submission/models/workspaceitem-section-form.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-form.model.ts @@ -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[]; } diff --git a/src/app/core/submission/models/workspaceitem-section-license.model.ts b/src/app/core/submission/models/workspaceitem-section-license.model.ts index 4a86503a04..26f625871e 100644 --- a/src/app/core/submission/models/workspaceitem-section-license.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-license.model.ts @@ -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; } diff --git a/src/app/core/submission/models/workspaceitem-section-recycle.model.ts b/src/app/core/submission/models/workspaceitem-section-recycle.model.ts deleted file mode 100644 index 760114e73a..0000000000 --- a/src/app/core/submission/models/workspaceitem-section-recycle.model.ts +++ /dev/null @@ -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[]; -} diff --git a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts index a42a334b86..177473b7d5 100644 --- a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts @@ -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[]; } diff --git a/src/app/core/submission/models/workspaceitem-section-upload.model.ts b/src/app/core/submission/models/workspaceitem-section-upload.model.ts index b936b5d4d8..f98e0584eb 100644 --- a/src/app/core/submission/models/workspaceitem-section-upload.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-upload.model.ts @@ -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[]; } diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index e954c880c4..165e69869c 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -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; diff --git a/src/app/core/submission/models/workspaceitem.model.ts b/src/app/core/submission/models/workspaceitem.model.ts index e927431d71..6548191ba2 100644 --- a/src/app/core/submission/models/workspaceitem.model.ts +++ b/src/app/core/submission/models/workspaceitem.model.ts @@ -1,5 +1,8 @@ import { SubmissionObject } from './submission-object.model'; +/** + * A model class for a Workspaceitem. + */ export class Workspaceitem extends SubmissionObject { } diff --git a/src/app/core/submission/submission-json-patch-operations.service.ts b/src/app/core/submission/submission-json-patch-operations.service.ts index f9371100d6..d469f2098f 100644 --- a/src/app/core/submission/submission-json-patch-operations.service.ts +++ b/src/app/core/submission/submission-json-patch-operations.service.ts @@ -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 { protected linkPath = ''; diff --git a/src/app/core/submission/submission-resource-type.ts b/src/app/core/submission/submission-resource-type.ts index 8718fc55d3..f5b8e2c423 100644 --- a/src/app/core/submission/submission-resource-type.ts +++ b/src/app/core/submission/submission-resource-type.ts @@ -11,7 +11,6 @@ export enum SubmissionResourceType { Group = 'group', WorkspaceItem = 'workspaceitem', WorkflowItem = 'workflowitem', - EditItem = 'edititem', SubmissionDefinitions = 'submissiondefinitions', SubmissionDefinition = 'submissiondefinition', SubmissionForm = 'submissionform', diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index abce34808e..20dfb43cbd 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -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(data: any, requestHref: string): any[] { const dataDefinition = this.process(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 diff --git a/src/app/submission/submission-rest.service.spec.ts b/src/app/core/submission/submission-rest.service.spec.ts similarity index 83% rename from src/app/submission/submission-rest.service.spec.ts rename to src/app/core/submission/submission-rest.service.spec.ts index c5992d7d10..6e748c5575 100644 --- a/src/app/submission/submission-rest.service.spec.ts +++ b/src/app/core/submission/submission-rest.service.spec.ts @@ -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; diff --git a/src/app/submission/submission-rest.service.ts b/src/app/core/submission/submission-rest.service.ts similarity index 60% rename from src/app/submission/submission-rest.service.ts rename to src/app/core/submission/submission-rest.service.ts index b5d563549f..e2b8bb01c8 100644 --- a/src/app/submission/submission-rest.service.ts +++ b/src/app/core/submission/submission-rest.service.ts @@ -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 + * server response + */ protected fetchRequest(requestId: string): Observable { 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 + * server response + */ public deleteById(scopeId: string, linkName?: string): Observable { 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 { + /** + * Return an existing submission Object from the server + * + * @param linkName + * The endpoint link name + * @param id + * The submission Object to retrieve + * @return Observable + * server response + */ + public getDataById(linkName: string, id: string): Observable { 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 + * server response + */ public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable { 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 + * server response + */ public patchToEndpoint(linkName: string, body: any, scopeId?: string): Observable { 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()); } diff --git a/src/app/core/submission/submission-scope-type.ts b/src/app/core/submission/submission-scope-type.ts index 80d57c853f..6ed32d3b4e 100644 --- a/src/app/core/submission/submission-scope-type.ts +++ b/src/app/core/submission/submission-scope-type.ts @@ -1,5 +1,4 @@ export enum SubmissionScopeType { WorkspaceItem = 'WORKSPACE', - WorkflowItem = 'WORKFLOW', - EditItem = 'ITEM', + WorkflowItem = 'WORKFLOW' } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 266d2b5411..e739a62e81 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -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 { protected linkPath = 'workflowitems'; diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 119bfb66cc..3bb3eb1ee8 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -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 { protected linkPath = 'workspaceitems'; diff --git a/src/app/pagenotfound/pagenotfound.component.ts b/src/app/pagenotfound/pagenotfound.component.ts index a3a55c26bf..6e173b4139 100644 --- a/src/app/pagenotfound/pagenotfound.component.ts +++ b/src/app/pagenotfound/pagenotfound.component.ts @@ -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(); } diff --git a/src/app/shared/alerts/alerts.component.html b/src/app/shared/alert/alert.component.html similarity index 100% rename from src/app/shared/alerts/alerts.component.html rename to src/app/shared/alert/alert.component.html diff --git a/src/app/shared/alerts/alerts.component.scss b/src/app/shared/alert/alert.component.scss similarity index 100% rename from src/app/shared/alerts/alerts.component.scss rename to src/app/shared/alert/alert.component.scss diff --git a/src/app/shared/alert/alert.component.spec.ts b/src/app/shared/alert/alert.component.spec.ts new file mode 100644 index 0000000000..e235e27b28 --- /dev/null +++ b/src/app/shared/alert/alert.component.spec.ts @@ -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; + + 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; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + 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; +} diff --git a/src/app/shared/alerts/alerts.component.ts b/src/app/shared/alert/alert.component.ts similarity index 57% rename from src/app/shared/alerts/alerts.component.ts rename to src/app/shared/alert/alert.component.ts index c9fc0ec9cc..93535d2057 100644 --- a/src/app/shared/alerts/alerts.component.ts +++ b/src/app/shared/alert/alert.component.ts @@ -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 = new EventEmitter(); + /** + * 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'; diff --git a/src/app/shared/alerts/aletrs-type.ts b/src/app/shared/alert/aletr-type.ts similarity index 100% rename from src/app/shared/alerts/aletrs-type.ts rename to src/app/shared/alert/aletr-type.ts diff --git a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts index da62204d10..6362daf3c7 100644 --- a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts +++ b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts @@ -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 = new EventEmitter(); + /** + * 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'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index fc2c788c02..455c1075ef 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -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); } } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index ea151726f4..4d8123a4b9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -1,12 +1,12 @@ - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 56524523d1..9d56d7d1b3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -9,7 +9,7 @@ - + (ngbEvent)="onCustomEvent($event, null, true)"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts index 6078d2b5a9..f51c2f78f4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts @@ -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 { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html index c8443c640d..6fede2eff0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html @@ -5,7 +5,7 @@ [formGroupName]="model.id" [ngClass]="getClass('element','control')"> - + (ngbEvent)="onCustomEvent($event, null, true)"> diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 9356f86e8c..ee354d504f 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -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); } }); diff --git a/src/app/shared/mocks/mock-form-operations-service.ts b/src/app/shared/mocks/mock-form-operations-service.ts index ce8ca6d4e5..6fb6127087 100644 --- a/src/app/shared/mocks/mock-form-operations-service.ts +++ b/src/app/shared/mocks/mock-form-operations-service.ts @@ -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'), diff --git a/src/app/shared/mocks/mock-form-service.ts b/src/app/shared/mocks/mock-form-service.ts index 31455f03da..d0510f3a68 100644 --- a/src/app/shared/mocks/mock-form-service.ts +++ b/src/app/shared/mocks/mock-form-service.ts @@ -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) }); diff --git a/src/app/shared/mocks/mock-router.ts b/src/app/shared/mocks/mock-router.ts index f1357104b4..99bae4ef8f 100644 --- a/src/app/shared/mocks/mock-router.ts +++ b/src/app/shared/mocks/mock-router.ts @@ -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 = { diff --git a/src/app/shared/mocks/mock-scroll-to-service.ts b/src/app/shared/mocks/mock-scroll-to-service.ts index 9c600e405e..03b68e55d2 100644 --- a/src/app/shared/mocks/mock-scroll-to-service.ts +++ b/src/app/shared/mocks/mock-scroll-to-service.ts @@ -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') diff --git a/src/app/shared/mocks/mock-section-upload.service.ts b/src/app/shared/mocks/mock-section-upload.service.ts index 6b27c11ac5..9098fa64c0 100644 --- a/src/app/shared/mocks/mock-section-upload.service.ts +++ b/src/app/shared/mocks/mock-section-upload.service.ts @@ -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'), diff --git a/src/app/shared/mocks/mock-submission.ts b/src/app/shared/mocks/mock-submission.ts index 26f9c757ca..922e6ad02d 100644 --- a/src/app/shared/mocks/mock-submission.ts +++ b/src/app/shared/mocks/mock-submission.ts @@ -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 = [ { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index cef04918c8..94d9e73e5c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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, diff --git a/src/app/shared/utils/object-ngfor.pipe.ts b/src/app/shared/utils/object-ngfor.pipe.ts index 4715d5c151..982e3342e0 100644 --- a/src/app/shared/utils/object-ngfor.pipe.ts +++ b/src/app/shared/utils/object-ngfor.pipe.ts @@ -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 : + * + *
+ * {{obj.key}} - {{obj.value}} + *
+ * + */ @Pipe({ name: 'dsObjNgFor' }) diff --git a/src/app/submission/edit/submission-edit.component.html b/src/app/submission/edit/submission-edit.component.html index 21b20997cf..dcd8d84edc 100644 --- a/src/app/submission/edit/submission-edit.component.html +++ b/src/app/submission/edit/submission-edit.component.html @@ -1,7 +1,7 @@
- +
diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index d128191d79..60c8b9a7a3 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -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 diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index fa54280aae..2fe424bd3f 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -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 = new EventEmitter(); + /** + * A boolean representing if this dropdown button is disabled + * @type {BehaviorSubject} + */ public disabled$ = new BehaviorSubject(true); - public model: any; + + /** + * The search form control + * @type {FormControl} + */ public searchField: FormControl = new FormControl(); + + /** + * The collection list obtained from a search + * @type {Observable} + */ public searchListCollection$: Observable; + + /** + * The selected collection id + * @type {string} + */ public selectedCollectionId: string; + + /** + * The selected collection name + * @type {Observable} + */ public selectedCollectionName$: Observable; + /** + * 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(); diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index fb3a810501..5fbfd84cb8 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -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', () => { diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 5f2f36cc22..4f4e355397 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -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} + */ public processingDepositStatus: Observable; + + /** + * A boolean representing if a submission save operation is pending + * @type {Observable} + */ public processingSaveStatus: Observable; + + /** + * A boolean representing if showing deposit and discard buttons + * @type {Observable} + */ public showDepositAndDiscard: Observable; + + /** + * A boolean representing if submission form is valid or not + * @type {Observable} + */ private submissionIsInvalid: Observable = 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) => { diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.ts b/src/app/submission/form/section-add/submission-form-section-add.component.ts index 20db74feac..48ba07dad1 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.ts +++ b/src/app/submission/form/section-add/submission-form-section-add.component.ts @@ -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} + */ public sectionList$: Observable; + + /** + * A boolean representing if there are available sections to add + * @type {Observable} + */ public hasSections$: Observable; + /** + * 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); } diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 21f92c9cf1..31e46e2ccc 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -24,9 +24,9 @@
- +
- - + + [sectionId]="sectionId"> diff --git a/src/app/submission/sections/upload/file/file.component.scss b/src/app/submission/sections/upload/file/section-upload-file.component.scss similarity index 100% rename from src/app/submission/sections/upload/file/file.component.scss rename to src/app/submission/sections/upload/file/section-upload-file.component.scss diff --git a/src/app/submission/sections/upload/file/file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts similarity index 84% rename from src/app/submission/sections/upload/file/file.component.spec.ts rename to src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index 8557efd451..f87aa7d703 100644 --- a/src/app/submission/sections/upload/file/file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -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; + let fixture: ComponentFixture; 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 = 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(); diff --git a/src/app/submission/sections/upload/file/file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts similarity index 64% rename from src/app/submission/sections/upload/file/file.component.ts rename to src/app/submission/sections/upload/file/section-upload-file.component.ts index adf035bf23..9923c358e7 100644 --- a/src/app/submission/sections/upload/file/file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -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; - @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; + + /** + * 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} + */ public processingDelete$ = new BehaviorSubject(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(); diff --git a/src/app/submission/sections/upload/file/view/file-view.component.html b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html similarity index 88% rename from src/app/submission/sections/upload/file/view/file-view.component.html rename to src/app/submission/sections/upload/file/view/section-upload-file-view.component.html index 838e6d3b97..65b4b6379b 100644 --- a/src/app/submission/sections/upload/file/view/file-view.component.html +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html @@ -25,5 +25,5 @@ - + diff --git a/src/app/submission/sections/upload/file/view/file-view.component.spec.ts b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts similarity index 73% rename from src/app/submission/sections/upload/file/view/file-view.component.spec.ts rename to src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts index f5e924f309..87b025e6a9 100644 --- a/src/app/submission/sections/upload/file/view/file-view.component.spec.ts +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts @@ -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; + let fixture: ComponentFixture; 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 = ` - `; + `; testFixture = createTestComponent(html, TestComponent) as ComponentFixture; 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; }); diff --git a/src/app/submission/sections/upload/file/view/file-view.component.ts b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts similarity index 52% rename from src/app/submission/sections/upload/file/view/file-view.component.ts rename to src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts index 0e94608069..bb2fea20f8 100644 --- a/src/app/submission/sections/upload/file/view/file-view.component.ts +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts @@ -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); } } diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index 053f4e1d8e..d63bc1b7d6 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -15,17 +15,14 @@
- - {{ 'submission.sections.upload.header.policy.default.nolist' | translate:{ "collectionName": collectionName } }} - {{ 'submission.sections.upload.header.policy.default.withlist' | translate:{ "collectionName": collectionName } }} - +
diff --git a/src/app/submission/sections/upload/section-upload.component.spec.ts b/src/app/submission/sections/upload/section-upload.component.spec.ts index 1c64d673d3..be8f096964 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -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; + let fixture: ComponentFixture; 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']); diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 1caff1b726..3a79a670ad 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -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; - /* + /** * 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; // Groups for any policy + protected availableGroups: Map; // 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> = []; + // Retrieve Groups for accessCondition Policies this.availableAccessConditionOptions.forEach((accessCondition: AccessConditionOption) => { if (accessCondition.hasEndDate === true || accessCondition.hasStartDate === true) { - groups$.push( - this.groupService.findById(accessCondition.groupUUID).pipe( - find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded)) - ); + if (accessCondition.groupUUID) { + mapGroups$.push( + this.groupService.findById(accessCondition.groupUUID).pipe( + find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded), + map((rd: RemoteData) => ({ + accessCondition: accessCondition.name, + groups: [rd.payload] + } as AccessConditionGroupsMapEntry))) + ); + } else if (accessCondition.selectGroupUUID) { + mapGroups$.push( + this.groupService.findById(accessCondition.selectGroupUUID).pipe( + find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded), + flatMap((group: RemoteData) => group.payload.groups), + find((rd: RemoteData>) => !rd.isResponsePending && rd.hasSucceeded), + map((rd: RemoteData>) => ({ + accessCondition: accessCondition.name, + groups: rd.payload.page + } as AccessConditionGroupsMapEntry)) + )); + } } }); - return groups$; + return mapGroups$; }), - flatMap((group) => group), - reduce((acc: Group[], group: RemoteData) => { - 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[]]) => { @@ -159,24 +226,32 @@ export class UploadSectionComponent extends SectionModelComponent { }), distinctUntilChanged()) .subscribe(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => { - this.fileList = []; - this.fileIndexes = []; - this.fileNames = []; - this.changeDetectorRef.detectChanges(); - if (isNotUndefined(fileList) && fileList.length > 0) { - fileList.forEach((file) => { - this.fileList.push(file); - this.fileIndexes.push(file.uuid); - this.fileNames.push(this.getFileName(configMetadataForm, file)); - }); - } + this.fileList = []; + this.fileIndexes = []; + this.fileNames = []; + this.changeDetectorRef.detectChanges(); + if (isNotUndefined(fileList) && fileList.length > 0) { + fileList.forEach((file) => { + this.fileList.push(file); + this.fileIndexes.push(file.uuid); + this.fileNames.push(this.getFileName(configMetadataForm, file)); + }); + } - this.changeDetectorRef.detectChanges(); - } - ) + this.changeDetectorRef.detectChanges(); + } + ) ); } + /** + * Return file name from metadata + * + * @param configMetadataForm + * the bitstream's form configuration + * @param fileData + * the file metadata + */ private getFileName(configMetadataForm: SubmissionFormsModel, fileData: any): string { 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 + * the section status + */ protected getSectionStatus(): Observable { return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe( map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0))); diff --git a/src/app/submission/sections/upload/section-upload.service.ts b/src/app/submission/sections/upload/section-upload.service.ts index ab18807fa5..a851fa9daf 100644 --- a/src/app/submission/sections/upload/section-upload.service.ts +++ b/src/app/submission/sections/upload/section-upload.service.ts @@ -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} store + */ constructor(private store: Store) {} + /** + * 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 { return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)).pipe( map((state) => state), distinctUntilChanged()); } - public getFileData(submissionId: string, sectionId: string, fileUuid: string): Observable { + /** + * 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 { 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 { - 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 { + 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) ); } } diff --git a/src/app/submission/selectors.ts b/src/app/submission/selectors.ts index b52c44b7b1..51c960b537 100644 --- a/src/app/submission/selectors.ts +++ b/src/app/submission/selectors.ts @@ -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(parentSelector: Selector, subState: string, key: string): MemoizedSelector { return createSelector(parentSelector, (state: T) => { if (hasValue(state) && hasValue(state[subState])) { @@ -15,6 +17,9 @@ export function keySelector(parentSelector: Selector, subState: }); } +/** + * Export a function to return a subset of the state + */ export function subStateSelector(parentSelector: Selector, subState: string): MemoizedSelector { return createSelector(parentSelector, (state: T) => { if (hasValue(state) && hasValue(state[subState])) { diff --git a/src/app/submission/server-submission.service.ts b/src/app/submission/server-submission.service.ts index f9382af8d0..3aa55a9d58 100644 --- a/src/app/submission/server-submission.service.ts +++ b/src/app/submission/server-submission.service.ts @@ -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 + * observable of SubmissionObject + */ createSubmission(): Observable { return observableOf(null); } + /** + * Override retrieveSubmission parent method to return an empty observable + * + * @return Observable + * observable of SubmissionObject + */ retrieveSubmission(submissionId): Observable> { 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; } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 0cd290a82f..e6c24226e2 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -3,30 +3,29 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; -import { FormSectionComponent } from './sections/form/section-form.component'; +import { SubmissionSectionformComponent } from './sections/form/section-form.component'; import { SectionsDirective } from './sections/sections.directive'; import { SectionsService } from './sections/sections.service'; import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component'; import { SubmissionFormFooterComponent } from './form/footer/submission-form-footer.component'; import { SubmissionFormComponent } from './form/submission-form.component'; import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; -import { SectionContainerComponent } from './sections/container/section-container.component'; +import { SubmissionSectionContainerComponent } from './sections/container/section-container.component'; import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { submissionReducers } from './submission.reducers'; import { submissionEffects } from './submission.effects'; -import { UploadSectionComponent } from './sections/upload/section-upload.component'; +import { SubmissionSectionUploadComponent } from './sections/upload/section-upload.component'; import { SectionUploadService } from './sections/upload/section-upload.service'; import { SubmissionUploadFilesComponent } from './form/submission-upload-files/submission-upload-files.component'; -import { SubmissionRestService } from './submission-rest.service'; -import { LicenseSectionComponent } from './sections/license/section-license.component'; +import { SubmissionSectionLicenseComponent } from './sections/license/section-license.component'; import { SubmissionUploadsConfigService } from '../core/config/submission-uploads-config.service'; import { SubmissionEditComponent } from './edit/submission-edit.component'; -import { UploadSectionFileComponent } from './sections/upload/file/file.component'; -import { UploadSectionFileEditComponent } from './sections/upload/file/edit/file-edit.component'; -import { UploadSectionFileViewComponent } from './sections/upload/file/view/file-view.component'; -import { AccessConditionsComponent } from './sections/upload/accessConditions/accessConditions.component'; +import { SubmissionSectionUploadFileComponent } from './sections/upload/file/section-upload-file.component'; +import { SubmissionSectionUploadFileEditComponent } from './sections/upload/file/edit/section-upload-file-edit.component'; +import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file/view/section-upload-file-view.component'; +import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSubmitComponent } from './submit/submission-submit.component'; @NgModule({ @@ -39,12 +38,12 @@ import { SubmissionSubmitComponent } from './submit/submission-submit.component' TranslateModule ], declarations: [ - AccessConditionsComponent, - UploadSectionComponent, - FormSectionComponent, - LicenseSectionComponent, + SubmissionSectionUploadAccessConditionsComponent, + SubmissionSectionUploadComponent, + SubmissionSectionformComponent, + SubmissionSectionLicenseComponent, SectionsDirective, - SectionContainerComponent, + SubmissionSectionContainerComponent, SubmissionEditComponent, SubmissionFormSectionAddComponent, SubmissionFormCollectionComponent, @@ -52,15 +51,15 @@ import { SubmissionSubmitComponent } from './submit/submission-submit.component' SubmissionFormFooterComponent, SubmissionSubmitComponent, SubmissionUploadFilesComponent, - UploadSectionFileComponent, - UploadSectionFileEditComponent, - UploadSectionFileViewComponent + SubmissionSectionUploadFileComponent, + SubmissionSectionUploadFileEditComponent, + SubmissionSectionUploadFileViewComponent ], entryComponents: [ - UploadSectionComponent, - FormSectionComponent, - LicenseSectionComponent, - SectionContainerComponent], + SubmissionSectionUploadComponent, + SubmissionSectionformComponent, + SubmissionSectionLicenseComponent, + SubmissionSectionContainerComponent], exports: [ SubmissionEditComponent, SubmissionFormComponent, @@ -69,9 +68,12 @@ import { SubmissionSubmitComponent } from './submit/submission-submit.component' providers: [ SectionUploadService, SectionsService, - SubmissionRestService, SubmissionUploadsConfigService ] }) + +/** + * This module handles all components that are necessary for the submission process + */ export class SubmissionModule { } diff --git a/src/app/submission/submission.reducers.ts b/src/app/submission/submission.reducers.ts index 39069b2917..939c4654ad 100644 --- a/src/app/submission/submission.reducers.ts +++ b/src/app/submission/submission.reducers.ts @@ -5,6 +5,9 @@ import { SubmissionObjectState } from './objects/submission-objects.reducer'; +/** + * The Submission State + */ export interface SubmissionState { 'objects': SubmissionObjectState } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 7dde19a306..d764f09538 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -11,7 +11,7 @@ import { cold, getTestScheduler, hot, } from 'jasmine-marbles'; import { MockRouter } from '../shared/mocks/mock-router'; import { SubmissionService } from './submission.service'; import { submissionReducers } from './submission.reducers'; -import { SubmissionRestService } from './submission-rest.service'; +import { SubmissionRestService } from '../core/submission/submission-rest.service'; import { RouteService } from '../shared/services/route.service'; import { SubmissionRestServiceStub } from '../shared/testing/submission-rest-service-stub'; import { MockActivatedRoute } from '../shared/mocks/mock-active-router'; @@ -660,9 +660,6 @@ describe('SubmissionService test suite', () => { router.setRoute('/workflowitems/826/edit'); expect(service.getSubmissionScope()).toBe(expected); - expected = SubmissionScopeType.EditItem; - router.setRoute('/items/9e79b1f2-ae0f-4737-9a4b-990952a8857c/edit'); - expect(service.getSubmissionScope()).toBe(expected); }); }); @@ -777,7 +774,7 @@ describe('SubmissionService test suite', () => { service.notifyNewSection(submissionId, sectionId); flush(); - expect((service as any).notificationsService.info).toHaveBeenCalledWith(null, 'test', null, true); + expect((service as any).notificationsService.info).toHaveBeenCalledWith(null, 'submission.sections.general.metadata-extracted-new-section', null, true); })); }); @@ -890,7 +887,7 @@ describe('SubmissionService test suite', () => { const duration = config.submission.autosave.timer * (1000 * 60); service.startAutoSave('826'); - const sub = (service as any).timerObs.subscribe(); + const sub = (service as any).timer$.subscribe(); tick(duration / 2); expect((service as any).store.dispatch).not.toHaveBeenCalled(); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index dab6a9d9d5..82185a8eae 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -31,7 +31,7 @@ import { submissionObjectFromIdSelector } from './selectors'; import { GlobalConfig } from '../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../config'; import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; -import { SubmissionRestService } from './submission-rest.service'; +import { SubmissionRestService } from '../core/submission/submission-rest.service'; import { SectionDataObject } from './sections/models/section-data.model'; import { SubmissionScopeType } from '../core/submission/submission-scope-type'; import { SubmissionObject } from '../core/submission/models/submission-object.model'; @@ -44,12 +44,32 @@ import { RemoteData } from '../core/data/remote-data'; import { ErrorResponse } from '../core/cache/response.models'; import { RemoteDataError } from '../core/data/remote-data-error'; +/** + * A service that provides methods used in submission process. + */ @Injectable() export class SubmissionService { + /** + * Subscription + */ protected autoSaveSub: Subscription; - protected timerObs: Observable; + /** + * Observable used as timer + */ + protected timer$: Observable; + + /** + * Initialize service variables + * @param {GlobalConfig} EnvConfig + * @param {NotificationsService} notificationsService + * @param {SubmissionRestService} restService + * @param {Router} router + * @param {RouteService} routeService + * @param {Store} store + * @param {TranslateService} translate + */ constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected notificationsService: NotificationsService, protected restService: SubmissionRestService, @@ -59,28 +79,74 @@ export class SubmissionService { protected translate: TranslateService) { } + /** + * Dispatch a new [ChangeSubmissionCollectionAction] + * + * @param submissionId + * The submission id + * @param collectionId + * The collection id + */ changeSubmissionCollection(submissionId, collectionId) { this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId)); } + /** + * Perform a REST call to create a new workspaceitem and return response + * + * @return Observable + * observable of SubmissionObject + */ createSubmission(): Observable { return this.restService.postToEndpoint('workspaceitems', {}).pipe( map((workspaceitem: SubmissionObject) => workspaceitem[0]), catchError(() => observableOf({}))) } - depositSubmission(selfUrl: string): Observable { + /** + * Perform a REST call to deposit a workspaceitem and return response + * + * @param selfUrl + * The workspaceitem self url + * @return Observable + * observable of SubmissionObject + */ + depositSubmission(selfUrl: string): Observable { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); options.headers = headers; - return this.restService.postToEndpoint('workflowitems', selfUrl, null, options); + return this.restService.postToEndpoint('workflowitems', selfUrl, null, options) as Observable; } - discardSubmission(submissionId: string): Observable { - return this.restService.deleteById(submissionId); + /** + * Perform a REST call to delete a workspaceitem and return response + * + * @param submissionId + * The submission id + * @return Observable + * observable of SubmissionObject + */ + discardSubmission(submissionId: string): Observable { + return this.restService.deleteById(submissionId) as Observable; } + /** + * Dispatch a new [InitSubmissionFormAction] + * + * @param collectionId + * The collection id + * @param submissionId + * The submission id + * @param selfUrl + * The workspaceitem self url + * @param submissionDefinition + * The [SubmissionDefinitionsModel] that define submission configuration + * @param sections + * The [WorkspaceitemSectionsObject] that define submission sections init data + * @param errors + * The [SubmissionSectionError] that define submission sections init errors + */ dispatchInit( collectionId: string, submissionId: string, @@ -91,36 +157,92 @@ export class SubmissionService { this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, errors)); } + /** + * Dispatch a new [SaveAndDepositSubmissionAction] + * + * @param submissionId + * The submission id + */ dispatchDeposit(submissionId) { this.store.dispatch(new SaveAndDepositSubmissionAction(submissionId)); } + /** + * Dispatch a new [DiscardSubmissionAction] + * + * @param submissionId + * The submission id + */ dispatchDiscard(submissionId) { this.store.dispatch(new DiscardSubmissionAction(submissionId)); } + /** + * Dispatch a new [SaveSubmissionFormAction] + * + * @param submissionId + * The submission id + */ dispatchSave(submissionId) { this.store.dispatch(new SaveSubmissionFormAction(submissionId)); } + /** + * Dispatch a new [SaveForLaterSubmissionFormAction] + * + * @param submissionId + * The submission id + */ dispatchSaveForLater(submissionId) { this.store.dispatch(new SaveForLaterSubmissionFormAction(submissionId)); } + /** + * Dispatch a new [SaveSubmissionSectionFormAction] + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ dispatchSaveSection(submissionId, sectionId) { this.store.dispatch(new SaveSubmissionSectionFormAction(submissionId, sectionId)); } + /** + * Return the id of the current focused section for the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable of section id + */ getActiveSectionId(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((submission: SubmissionObjectEntry) => submission.activeSection)); } + /** + * Return the [SubmissionObjectEntry] for the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable of SubmissionObjectEntry + */ getSubmissionObject(submissionId: string): Observable { return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( filter((submission: SubmissionObjectEntry) => isNotUndefined(submission))); } + /** + * Return a list of the active [SectionDataObject] belonging to the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with the list of active submission's sections + */ getSubmissionSections(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( find((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading), @@ -146,6 +268,14 @@ export class SubmissionService { distinctUntilChanged()); } + /** + * Return a list of the disabled [SectionDataObject] belonging to the specified submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with the list of disabled submission's sections + */ getDisabledSectionsList(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( filter((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading), @@ -167,6 +297,12 @@ export class SubmissionService { distinctUntilChanged()); } + /** + * Return the correct REST endpoint link path depending on the page route + * + * @return string + * link path + */ getSubmissionObjectLinkName(): string { const url = this.router.routerState.snapshot.url; if (url.startsWith('/workspaceitems') || url.startsWith('/submit')) { @@ -178,6 +314,12 @@ export class SubmissionService { } } + /** + * Return the submission scope + * + * @return SubmissionScopeType + * the SubmissionScopeType + */ getSubmissionScope(): SubmissionScopeType { let scope: SubmissionScopeType; switch (this.getSubmissionObjectLinkName()) { @@ -187,13 +329,18 @@ export class SubmissionService { case 'workflowitems': scope = SubmissionScopeType.WorkflowItem; break; - case 'edititems': - scope = SubmissionScopeType.EditItem; - break; } return scope; } + /** + * Return the validity status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission validity status + */ getSubmissionStatus(submissionId: string): Observable { return this.store.select(submissionSelector).pipe( map((submissions: SubmissionState) => submissions.objects[submissionId]), @@ -219,6 +366,14 @@ export class SubmissionService { startWith(false)); } + /** + * Return the save processing status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission save processing status + */ getSubmissionSaveProcessingStatus(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((state: SubmissionObjectEntry) => state.savePending), @@ -226,6 +381,14 @@ export class SubmissionService { startWith(false)); } + /** + * Return the deposit processing status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission deposit processing status + */ getSubmissionDepositProcessingStatus(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((state: SubmissionObjectEntry) => state.depositPending), @@ -233,27 +396,52 @@ export class SubmissionService { startWith(false)); } - isSectionHidden(sectionData: SubmissionSectionObject) { + /** + * Return the visibility status of the specified section + * + * @param sectionData + * The section data + * @return boolean + * true if section is hidden, false otherwise + */ + isSectionHidden(sectionData: SubmissionSectionObject): boolean { return (isNotUndefined(sectionData.visibility) && sectionData.visibility.main === 'HIDDEN' && sectionData.visibility.other === 'HIDDEN'); - } + /** + * Return the loading status of the submission + * + * @param submissionId + * The submission id + * @return Observable + * observable with submission loading status + */ isSubmissionLoading(submissionId: string): Observable { return this.getSubmissionObject(submissionId).pipe( map((submission: SubmissionObjectEntry) => submission.isLoading), distinctUntilChanged()); } + /** + * Show a notification when a new section is added to submission form + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param sectionType + * The section type + */ notifyNewSection(submissionId: string, sectionId: string, sectionType?: SectionsType) { - this.translate.get('submission.sections.general.metadata-extracted-new-section', { sectionId }).pipe( - first()) - .subscribe((m) => { - this.notificationsService.info(null, m, null, true); - }); + const m = this.translate.instant('submission.sections.general.metadata-extracted-new-section', { sectionId }); + this.notificationsService.info(null, m, null, true); } + /** + * Redirect to MyDspace page + */ redirectToMyDSpace() { this.routeService.getPreviousUrl().pipe( first() @@ -267,10 +455,27 @@ export class SubmissionService { } + /** + * Dispatch a new [CancelSubmissionFormAction] + */ resetAllSubmissionObjects() { this.store.dispatch(new CancelSubmissionFormAction()); } + /** + * Dispatch a new [ResetSubmissionFormAction] + * + * @param collectionId + * The collection id + * @param submissionId + * The submission id + * @param selfUrl + * The workspaceitem self url + * @param submissionDefinition + * The [SubmissionDefinitionsModel] that define submission configuration + * @param sections + * The [WorkspaceitemSectionsObject] that define submission sections init data + */ resetSubmissionObject( collectionId: string, submissionId: string, @@ -281,6 +486,12 @@ export class SubmissionService { this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition)); } + /** + * Perform a REST call to retrieve an existing workspaceitem/workflowitem and return response + * + * @return Observable> + * observable of RemoteData + */ retrieveSubmission(submissionId): Observable> { return this.restService.getDataById(this.getSubmissionObjectLinkName(), submissionId).pipe( find((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)), @@ -302,21 +513,38 @@ export class SubmissionService { ); } + /** + * Dispatch a new [SetActiveSectionAction] + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + */ setActiveSection(submissionId, sectionId) { this.store.dispatch(new SetActiveSectionAction(submissionId, sectionId)); } + /** + * Allow to save automatically the submission + * + * @param submissionId + * The submission id + */ startAutoSave(submissionId) { this.stopAutoSave(); // AUTOSAVE submission // Retrieve interval from config and convert to milliseconds const duration = this.EnvConfig.submission.autosave.timer * (1000 * 60); // Dispatch save action after given duration - this.timerObs = observableTimer(duration, duration); - this.autoSaveSub = this.timerObs + this.timer$ = observableTimer(duration, duration); + this.autoSaveSub = this.timer$ .subscribe(() => this.store.dispatch(new SaveSubmissionFormAction(submissionId))); } + /** + * Unsubscribe subscription to timer + */ stopAutoSave() { if (hasValue(this.autoSaveSub)) { this.autoSaveSub.unsubscribe(); diff --git a/src/app/submission/submit/submission-submit.component.html b/src/app/submission/submit/submission-submit.component.html index 7cd58069ac..c9e8c6b51a 100644 --- a/src/app/submission/submit/submission-submit.component.html +++ b/src/app/submission/submit/submission-submit.component.html @@ -1,8 +1,8 @@
- +
diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 2354c2b8ab..dbfd2f5a40 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -11,21 +11,56 @@ import { SubmissionService } from '../submission.service'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { Collection } from '../../core/shared/collection.model'; +/** + * This component allows to submit a new workspaceitem. + */ @Component({ - selector: 'ds-submit-page', + selector: 'ds-submission-submit', styleUrls: ['./submission-submit.component.scss'], templateUrl: './submission-submit.component.html' }) export class SubmissionSubmitComponent implements OnDestroy, OnInit { + /** + * The collection id this submission belonging to + * @type {string} + */ public collectionId: string; - public model: any; + + /** + * 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; + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ protected subs: Subscription[] = []; + /** + * Initialize instance variables + * + * @param {ChangeDetectorRef} changeDetectorRef + * @param {NotificationsService} notificationsService + * @param {SubmissionService} submissioService + * @param {Router} router + * @param {TranslateService} translate + * @param {ViewContainerRef} viewContainerRef + */ constructor(private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, private router: Router, @@ -34,6 +69,9 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { private viewContainerRef: ViewContainerRef) { } + /** + * Create workspaceitem on the server and initialize all instance variables + */ ngOnInit() { // NOTE execute the code on the browser side only, otherwise it is executed twice this.subs.push( @@ -56,6 +94,9 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { ) } + /** + * Unsubscribe from all subscriptions + */ ngOnDestroy() { this.subs .filter((subscription) => hasValue(subscription)) diff --git a/src/app/submission/utils/parseSectionErrorPaths.ts b/src/app/submission/utils/parseSectionErrorPaths.ts index b47b9d0b05..4c973dedcf 100644 --- a/src/app/submission/utils/parseSectionErrorPaths.ts +++ b/src/app/submission/utils/parseSectionErrorPaths.ts @@ -1,9 +1,28 @@ import { hasValue } from '../../shared/empty.util'; +/** + * An interface to represent the path of a section error + */ export interface SectionErrorPath { + + /** + * The section id + */ sectionId: string; + + /** + * The form field id + */ fieldId?: string; + + /** + * The form field index + */ fieldIndex?: number; + + /** + * The complete path + */ originalPath: string; } @@ -12,7 +31,7 @@ const regex = /([^\/]+)/g; const regexShort = /\/sections\/(.*)/; /** - * the following method accept an array of section path strings and return a path object + * The following method accept an array of section path strings and return a path object * @param {string | string[]} path * @returns {SectionErrorPath[]} */