1
0

Merge pull request #1475 from 4Science/CST-4506_item_embargo

Add submission section for item embargo
This commit is contained in:
Tim Donohue
2022-01-31 11:13:43 -06:00
committed by GitHub
35 changed files with 1379 additions and 84 deletions

View File

@@ -0,0 +1,45 @@
/**
* Model class for an Item Access Condition
*/
export class AccessesConditionOption {
/**
* The name for this Access Condition
*/
name: string;
/**
* The groupName for this Access Condition
*/
groupName: string;
/**
* A boolean representing if this Access Condition has a start date
*/
hasStartDate: boolean;
/**
* A boolean representing if this Access Condition has an end date
*/
hasEndDate: boolean;
/**
* Maximum value of the start date
*/
endDateLimit?: string;
/**
* Maximum value of the end date
*/
startDateLimit?: string;
/**
* Maximum value of the start date
*/
maxStartDate?: string;
/**
* Maximum value of the end date
*/
maxEndDate?: string;
}

View File

@@ -0,0 +1,42 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { ConfigObject } from './config.model';
import { AccessesConditionOption } from './config-accesses-conditions-options.model';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { HALLink } from '../../shared/hal-link.model';
/**
* Class for the configuration describing the item accesses condition
*/
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionAccessModel extends ConfigObject {
static type = SUBMISSION_ACCESSES_TYPE;
/**
* A list of available item access conditions
*/
@autoserialize
accessConditionOptions: AccessesConditionOption[];
/**
* Boolean that indicates whether the current item must be findable via search or browse.
*/
@autoserialize
discoverable: boolean;
/**
* Boolean that indicates whether or not the user can change the discoverable flag.
*/
@autoserialize
canChangeDiscoverable: boolean;
/**
* The links to all related resources returned by the rest api.
*/
@deserialize
_links: {
self: HALLink
};
}

View File

@@ -0,0 +1,10 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { SubmissionAccessModel } from './config-submission-access.model';
@typedObject
@inheritSerialization(SubmissionAccessModel)
export class SubmissionAccessesModel extends SubmissionAccessModel {
static type = SUBMISSION_ACCESSES_TYPE;
}

View File

@@ -15,3 +15,5 @@ export const SUBMISSION_SECTION_TYPE = new ResourceType('submissionsection');
export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads'); export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads');
export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload'); export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload');
export const SUBMISSION_ACCESSES_TYPE = new ResourceType('submissionaccessoption');

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { ConfigObject } from './models/config.model';
import { SubmissionAccessesModel } from './models/config-submission-accesses.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/**
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
*/
@Injectable()
@dataService(SUBMISSION_ACCESSES_TYPE)
export class SubmissionAccessesConfigService extends ConfigService {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<SubmissionAccessesModel>
) {
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionaccessoptions');
}
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionAccessesModel>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
}
}

View File

@@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { Action, StoreConfig, StoreModule } from '@ngrx/store';
@@ -165,6 +161,7 @@ import { Root } from './data/root.model';
import { SearchConfig } from './shared/search/search-filters/search-config.model'; import { SearchConfig } from './shared/search/search-filters/search-config.model';
import { SequenceService } from './shared/sequence.service'; import { SequenceService } from './shared/sequence.service';
import { GroupDataService } from './eperson/group-data.service'; import { GroupDataService } from './eperson/group-data.service';
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -347,7 +344,8 @@ export const models =
Registration, Registration,
UsageReport, UsageReport,
Root, Root,
SearchConfig SearchConfig,
SubmissionAccessesModel
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,25 @@
/**
* An interface to represent an access condition.
*/
export class AccessConditionObject {
/**
* The access condition id
*/
id: string;
/**
* The access condition name
*/
name: string;
/**
* Possible start date of the access condition
*/
startDate: string;
/**
* Possible end date of the access condition
*/
endDate: string;
}

View File

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

View File

@@ -0,0 +1,8 @@
import { AccessConditionObject } from './access-condition.model';
/**
* An interface to represent item's access condition.
*/
export class SubmissionItemAccessConditionObject extends AccessConditionObject {
}

View File

@@ -1,25 +1,8 @@
import { AccessConditionObject } from './access-condition.model';
/** /**
* An interface to represent bitstream's access condition. * An interface to represent bitstream's access condition.
*/ */
export class SubmissionUploadFileAccessConditionObject { export class SubmissionUploadFileAccessConditionObject extends AccessConditionObject {
/**
* The access condition id
*/
id: string;
/**
* The access condition name
*/
name: string;
/**
* Possible start date of the access condition
*/
startDate: string;
/**
* Possible end date of the access condition
*/
endDate: string;
} }

View File

@@ -0,0 +1,21 @@
import { SubmissionItemAccessConditionObject } from './submission-item-access-condition.model';
/**
* An interface to represent the submission's item accesses condition.
*/
export interface WorkspaceitemSectionAccessesObject {
/**
* The access condition id
*/
id: string;
/**
* Boolean that indicates whether the current item must be findable via search or browse.
*/
discoverable: boolean;
/**
* A list of available item access conditions
*/
accessConditions: SubmissionItemAccessConditionObject[];
}

View File

@@ -1,3 +1,4 @@
import { WorkspaceitemSectionAccessesObject } from './workspaceitem-section-accesses.model';
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
@@ -19,4 +20,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionFormObject | WorkspaceitemSectionFormObject
| WorkspaceitemSectionLicenseObject | WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionCcLicenseObject | WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
| string; | string;

View File

@@ -14,7 +14,8 @@
<div> <div>
<ng-container #componentViewContainer></ng-container> <ng-container #componentViewContainer></ng-container>
</div> </div>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
<small *ngIf="hasHint && ((!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small> class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first --> <!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null <div *ngIf="context?.index !== null

View File

@@ -15,8 +15,8 @@
[cdkDragDisabled]="dragDisabled" [cdkDragDisabled]="dragDisabled"
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'"> [cdkDragPreviewClass]="'ds-submission-reorder-dragging'">
<!-- Item content --> <!-- Item content -->
<div class="drag-handle" [class.invisible]="dragDisabled" tabindex="0"> <div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0">
<i class="drag-icon fas fa-grip-vertical fa-fw" ></i> <i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
</div> </div>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container> <ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
<ds-dynamic-form-control-container *ngFor="let _model of groupModel.group" <ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"

View File

@@ -3,7 +3,15 @@
:host { :host {
display: block; display: block;
} }
.drag-disable {
visibility: hidden !important;
&:hover, &:focus {
cursor: default;
.drag-icon {
visibility: hidden !important;
}
}
}
.cdk-drag { .cdk-drag {
margin-left: calc(-2.3 * var(--bs-spacer)); margin-left: calc(-2.3 * var(--bs-spacer));
margin-right: calc(-0.5 * var(--bs-spacer)); margin-right: calc(-0.5 * var(--bs-spacer));

View File

@@ -0,0 +1,87 @@
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
const configRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'canChangeDiscoverable': true,
'accessConditionOptions': [
{
'name': 'openaccess',
'hasStartDate': false,
'hasEndDate': false
},
{
'name': 'lease',
'hasStartDate': false,
'hasEndDate': true,
'maxEndDate': '2022-06-20T12:17:44.420+00:00'
},
{
'name': 'embargo',
'hasStartDate': true,
'hasEndDate': false,
'maxStartDate': '2024-12-20T12:17:44.420+00:00'
},
{
'name': 'administrator',
'hasStartDate': false,
'hasEndDate': false
}
],
'type': 'submissionaccessoption',
'_links': {
'self': {
'href': 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration'
}
}
});
const configResNotChangeDiscoverable = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'canChangeDiscoverable': false,
'accessConditionOptions': [
{
'name': 'openaccess',
'hasStartDate': false,
'hasEndDate': false
},
{
'name': 'lease',
'hasStartDate': false,
'hasEndDate': true,
'maxEndDate': '2022-06-20T12:17:44.420+00:00'
},
{
'name': 'embargo',
'hasStartDate': true,
'hasEndDate': false,
'maxStartDate': '2024-12-20T12:17:44.420+00:00'
},
{
'name': 'administrator',
'hasStartDate': false,
'hasEndDate': false
}
],
'type': 'submissionaccessoption',
'_links': {
'self': {
'href': 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration'
}
}
});
export function getSubmissionAccessesConfigService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SubmissionAccessesConfigService', {
findByHref: createSuccessfulRemoteDataObject$(configRes),
});
}
export function getSubmissionAccessesConfigNotChangeDiscoverableService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SubmissionAccessesConfigService', {
findByHref: createSuccessfulRemoteDataObject$(configResNotChangeDiscoverable),
});
}

View File

@@ -0,0 +1,13 @@
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { of as observableOf } from 'rxjs';
const dataRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'accessConditions': [],
});
export function getSectionAccessesService() {
return jasmine.createSpyObj('SectionAccessesService', {
getAccessesData: observableOf(dataRes),
});
}

View File

@@ -1654,7 +1654,7 @@ export const mockFileFormData = {
}, },
}, },
{ {
accessConditionGroup:{ accessConditionGroup: {
name: [ name: [
{ {
value: 'lease', value: 'lease',
@@ -1723,3 +1723,94 @@ export const mockFileFormData = {
} }
] ]
}; };
export const mockAccessesFormData = {
discoverable: true,
accessCondition: [
{
accessConditionGroup: {
name: [
{
value: 'openaccess',
language: null,
authority: null,
display: 'openaccess',
confidence: -1,
place: 0,
otherInformation: null
}
],
},
},
{
accessConditionGroup: {
name: [
{
value: 'lease',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
endDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
},
{
accessConditionGroup: {
name: [
{
value: 'embargo',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
startDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
}
]
};

View File

@@ -0,0 +1,100 @@
import { FormControl, FormGroup } from '@angular/forms';
import { DynamicCheckboxModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
export const accessConditionChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
returnValue: true,
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicSelectModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: { required: 'submission.sections.upload.form.date-required-until' },
hidden: false,
hint: null,
id: 'endDate',
label: 'submission.sections.upload.form.until-label',
labelTooltip: null,
name: 'endDate',
placeholder: 'Until',
prefix: null,
relations: [],
required: true,
suffix: null,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};
export const checkboxChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
returnValue: true,
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicCheckboxModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: null,
hidden: false,
hint: null,
id: 'discoverable',
indeterminate: false,
label: 'Discoverable',
labelPosition: null,
labelTooltip: null,
name: 'discoverable',
relations: [],
required: false,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};

View File

@@ -0,0 +1,7 @@
<ds-form *ngIf="!!formModel" #formRef="formComponent"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"
[displayCancel]="false"
(dfChange)="onChange($event)"
(removeArrayItem)="onRemove($event)"></ds-form>

View File

@@ -0,0 +1,5 @@
::ng-deep .access-condition-group {
position: relative;
top: -2.3rem;
margin-bottom: -2.3rem;
}

View File

@@ -0,0 +1,204 @@
import { FormService } from '../../../shared/form/form.service';
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { SubmissionSectionAccessesComponent } from './section-accesses.component';
import { SectionsService } from '../sections.service';
import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { SubmissionAccessesConfigService } from '../../../core/config/submission-accesses-config.service';
import {
getSubmissionAccessesConfigNotChangeDiscoverableService,
getSubmissionAccessesConfigService
} from '../../../shared/mocks/section-accesses-config.service.mock';
import { SectionAccessesService } from './section-accesses.service';
import { SectionFormOperationsService } from '../form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { getSectionAccessesService } from '../../../shared/mocks/section-accesses.service.mock';
import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub';
import { BrowserModule } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { Store } from '@ngrx/store';
import { FormComponent } from '../../../shared/form/form.component';
import {
DynamicCheckboxModel,
DynamicDatePickerModel,
DynamicFormArrayModel,
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { AppState } from '../../../app.reducer';
import { getMockFormService } from '../../../shared/mocks/form-service.mock';
import { mockAccessesFormData } from '../../../shared/mocks/submission.mock';
import { accessConditionChangeEvent, checkboxChangeEvent } from '../../../shared/testing/form-event.stub';
describe('SubmissionSectionAccessesComponent', () => {
let component: SubmissionSectionAccessesComponent;
let fixture: ComponentFixture<SubmissionSectionAccessesComponent>;
const sectionsServiceStub = new SectionsServiceStub();
// const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const builderService: FormBuilderService = getMockFormBuilderService();
const submissionAccessesConfigService = getSubmissionAccessesConfigService();
const sectionAccessesService = getSectionAccessesService();
const sectionFormOperationsService = getMockFormOperationsService();
const operationsBuilder = jasmine.createSpyObj('operationsBuilder', {
add: undefined,
remove: undefined,
replace: undefined,
});
let formService: any;
const storeStub = jasmine.createSpyObj('store', ['dispatch']);
const sectionData = {
header: 'submit.progressbar.accessCondition',
config: 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration',
mandatory: true,
sectionType: 'accessCondition',
collapsed: false,
enabled: true,
data: {
discoverable: true,
accessConditions: []
},
errorsToShow: [],
serverValidationErrors: [],
isLoading: false,
isValid: true
};
describe('First with canChangeDiscoverable true', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserModule,
TranslateModule.forRoot()
],
declarations: [SubmissionSectionAccessesComponent, FormComponent],
providers: [
{ provide: SectionsService, useValue: sectionsServiceStub },
{ provide: FormBuilderService, useValue: builderService },
{ provide: SubmissionAccessesConfigService, useValue: submissionAccessesConfigService },
{ provide: SectionAccessesService, useValue: sectionAccessesService },
{ provide: SectionFormOperationsService, useValue: sectionFormOperationsService },
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: FormService, useValue: getMockFormService() },
{ provide: Store, useValue: storeStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub },
{ provide: 'sectionDataProvider', useValue: sectionData },
{ provide: 'submissionIdProvider', useValue: '1508' },
]
})
.compileComponents();
});
beforeEach(inject([Store], (store: Store<AppState>) => {
fixture = TestBed.createComponent(SubmissionSectionAccessesComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockAccessesFormData));
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have created formModel', () => {
expect(component.formModel).toBeTruthy();
});
it('should have formModel length should be 2', () => {
expect(component.formModel.length).toEqual(2);
});
it('formModel should have 1 model type checkbox and 1 model type array', () => {
expect(component.formModel[0] instanceof DynamicCheckboxModel).toBeTrue();
expect(component.formModel[1] instanceof DynamicFormArrayModel).toBeTrue();
});
it('formModel type array should have formgroup with 1 input and 2 datepickers', () => {
const formModel: any = component.formModel[1];
const formGroup = formModel.groupFactory()[0].group;
expect(formGroup[0] instanceof DynamicSelectModel).toBeTrue();
expect(formGroup[1] instanceof DynamicDatePickerModel).toBeTrue();
expect(formGroup[2] instanceof DynamicDatePickerModel).toBeTrue();
});
it('when checkbox changed it should call operationsBuilder replace function', () => {
component.onChange(checkboxChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.replace).toHaveBeenCalled();
});
it('when dropdown select changed it should call operationsBuilder add function', () => {
component.onChange(accessConditionChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.add).toHaveBeenCalled();
});
});
describe('when canDescoverable is false', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserModule,
TranslateModule.forRoot()
],
declarations: [SubmissionSectionAccessesComponent, FormComponent],
providers: [
{ provide: SectionsService, useValue: sectionsServiceStub },
{ provide: FormBuilderService, useValue: builderService },
{ provide: SubmissionAccessesConfigService, useValue: getSubmissionAccessesConfigNotChangeDiscoverableService() },
{ provide: SectionAccessesService, useValue: sectionAccessesService },
{ provide: SectionFormOperationsService, useValue: sectionFormOperationsService },
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: FormService, useValue: getMockFormService() },
{ provide: Store, useValue: storeStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub },
{ provide: 'sectionDataProvider', useValue: sectionData },
{ provide: 'submissionIdProvider', useValue: '1508' },
]
})
.compileComponents();
});
beforeEach(inject([Store], (store: Store<AppState>) => {
fixture = TestBed.createComponent(SubmissionSectionAccessesComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockAccessesFormData));
fixture.detectChanges();
}));
it('should have formModel length should be 1', () => {
expect(component.formModel.length).toEqual(1);
});
it('formModel should have only 1 model type array', () => {
expect(component.formModel[0] instanceof DynamicFormArrayModel).toBeTrue();
});
});
});

View File

@@ -0,0 +1,379 @@
import { SectionAccessesService } from './section-accesses.service';
import { Component, Inject, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { filter, map, mergeMap, take } from 'rxjs/operators';
import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { renderSectionFor } from '../sections-decorator';
import { SectionsType } from '../sections-type';
import { SectionDataObject } from '../models/section-data.model';
import { SectionsService } from '../sections.service';
import { SectionModelComponent } from '../models/section.model';
import {
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DynamicCheckboxModel,
DynamicDatePickerModel,
DynamicFormArrayModel,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicSelectModel,
MATCH_ENABLED,
OR_OPERATOR
} from '@ng-dynamic-forms/core';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import {
ACCESS_CONDITION_GROUP_CONFIG,
ACCESS_CONDITION_GROUP_LAYOUT,
ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT,
ACCESS_FORM_CHECKBOX_CONFIG,
ACCESS_FORM_CHECKBOX_LAYOUT,
FORM_ACCESS_CONDITION_END_DATE_CONFIG,
FORM_ACCESS_CONDITION_END_DATE_LAYOUT,
FORM_ACCESS_CONDITION_START_DATE_CONFIG,
FORM_ACCESS_CONDITION_START_DATE_LAYOUT,
FORM_ACCESS_CONDITION_TYPE_CONFIG,
FORM_ACCESS_CONDITION_TYPE_LAYOUT
} from './section-accesses.model';
import { hasValue, isNotEmpty, isNotNull } from '../../../shared/empty.util';
import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model';
import { SubmissionAccessesConfigService } from '../../../core/config/submission-accesses-config.service';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { FormComponent } from '../../../shared/form/form.component';
import { FormService } from '../../../shared/form/form.service';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { SectionFormOperationsService } from '../form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { dateToISOFormat } from '../../../shared/date.util';
/**
* This component represents a section for managing item's access conditions.
*/
@Component({
selector: 'ds-section-accesses',
templateUrl: './section-accesses.component.html',
styleUrls: ['./section-accesses.component.scss']
})
@renderSectionFor(SectionsType.AccessesCondition)
export class SubmissionSectionAccessesComponent extends SectionModelComponent {
/**
* The FormComponent reference
*/
@ViewChild('formRef') public formRef: FormComponent;
/**
* List of available access conditions that could be set to item
*/
public availableAccessConditionOptions: AccessesConditionOption[]; // List of accessConditions that an user can select
/**
* The form id
* @type {string}
*/
public formId: string;
/**
* The accesses section data
* @type {WorkspaceitemSectionAccessesObject}
*/
public accessesData: WorkspaceitemSectionAccessesObject;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[];
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The [[JsonPatchOperationPathCombiner]] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* Defines if the access discoverable property can be managed
*/
public canChangeDiscoverable: boolean;
/**
* Initialize instance variables
*
* @param {SectionsService} sectionService
* @param {SectionDataObject} injectedSectionData
* @param {FormService} formService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SectionFormOperationsService} formOperationsService
* @param {FormBuilderService} formBuilderService
* @param {TranslateService} translate
* @param {SubmissionAccessesConfigService} accessesConfigService
* @param {SectionAccessesService} accessesService
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {string} injectedSubmissionId
*/
constructor(
protected sectionService: SectionsService,
private formBuilderService: FormBuilderService,
private accessesConfigService: SubmissionAccessesConfigService,
private accessesService: SectionAccessesService,
protected formOperationsService: SectionFormOperationsService,
protected operationsBuilder: JsonPatchOperationsBuilder,
private formService: FormService,
private translate: TranslateService,
private operationsService: SubmissionJsonPatchOperationsService,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(undefined, injectedSectionData, injectedSubmissionId);
}
/**
* Initialize form model values
*
* @param formModel
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
this.accessesData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
.forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) {
if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) {
const date = new Date(accessCondition[key]);
metadataModel.value = {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate()
};
} else {
metadataModel.value = accessCondition[key];
}
}
});
});
}
/**
* Method called when a form dfChange event is fired.
* Dispatch form operations based on changes.
*/
onChange(event: DynamicFormControlEvent) {
if (event.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX) {
const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
this.operationsBuilder.replace(this.pathCombiner.getPath(path), value.value, true);
} else {
if (event.model.id === FORM_ACCESS_CONDITION_TYPE_CONFIG.id) {
// Clear previous state when switching through different access conditions
const startDateControl: FormControl = event.control.parent.get('startDate') as FormControl;
const endDateControl: FormControl = event.control.parent.get('endDate') as FormControl;
startDateControl?.markAsUntouched();
endDateControl?.markAsUntouched();
startDateControl?.setValue(null);
endDateControl?.setValue(null);
event.control.parent.markAsDirty();
}
// validate form
this.formService.validateAllFormFields(this.formRef.formGroup);
this.formService.isValid(this.formId).pipe(
take(1),
filter((isValid) => isValid),
mergeMap(() => this.formService.getFormData(this.formId)),
take(1)
).subscribe((formData: any) => {
const accessConditionsToSave = [];
formData.accessCondition
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
if (accessConditionOpt) {
const currentAccessCondition = Object.assign({}, accessCondition);
currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name);
/* When start and end date fields are deactivated, their values may be still present in formData,
therefore it is necessary to delete them if they're not allowed by the current access condition option. */
if (!accessConditionOpt.hasStartDate) {
delete currentAccessCondition.startDate;
} else if (accessCondition.startDate) {
const startDate = this.retrieveValueFromField(accessCondition.startDate);
currentAccessCondition.startDate = dateToISOFormat(startDate);
}
if (!accessConditionOpt.hasEndDate) {
delete currentAccessCondition.endDate;
} else if (accessCondition.endDate) {
const endDate = this.retrieveValueFromField(accessCondition.endDate);
currentAccessCondition.endDate = dateToISOFormat(endDate);
}
accessConditionsToSave.push(currentAccessCondition);
}
});
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
});
}
}
/**
* Method called when a form removeArrayItem event is fired.
* Dispatch remove form operations based on changes.
*/
onRemove(event: DynamicFormControlEvent) {
const fieldIndex = this.formOperationsService.getArrayIndexFromEvent(event);
const fieldPath = 'accessConditions/' + fieldIndex;
this.operationsBuilder.remove(this.pathCombiner.getPath(fieldPath));
}
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
/**
* Initialize all instance variables and retrieve collection default access conditions
*/
protected onSectionInit(): void {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
this.formId = this.formService.getUniqueId(this.sectionData.id);
const config$ = this.accessesConfigService.findByHref(this.sectionData.config, true, false).pipe(
getFirstSucceededRemoteData(),
map((config) => config.payload),
);
const accessData$ = this.accessesService.getAccessesData(this.submissionId, this.sectionData.id);
combineLatest([config$, accessData$]).subscribe(([config, accessData]) => {
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
this.canChangeDiscoverable = !!config.canChangeDiscoverable;
this.accessesData = accessData;
this.formModel = this.buildFileEditForm();
});
}
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> {
return of(true);
}
/**
* Initialize form model
*/
protected buildFileEditForm() {
const formModel: DynamicFormControlModel[] = [];
if (this.canChangeDiscoverable) {
const discoverableCheckboxConfig = Object.assign({}, ACCESS_FORM_CHECKBOX_CONFIG, {
label: this.translate.instant('submission.sections.accesses.form.discoverable-label'),
hint: this.translate.instant('submission.sections.accesses.form.discoverable-description'),
value: this.accessesData.discoverable
});
formModel.push(
new DynamicCheckboxModel(discoverableCheckboxConfig, ACCESS_FORM_CHECKBOX_LAYOUT)
);
}
const accessConditionTypeModelConfig = Object.assign({}, FORM_ACCESS_CONDITION_TYPE_CONFIG);
const accessConditionsArrayConfig = Object.assign({}, ACCESS_CONDITIONS_FORM_ARRAY_CONFIG);
const accessConditionTypeOptions = [];
for (const accessCondition of this.availableAccessConditionOptions) {
accessConditionTypeOptions.push(
{
label: accessCondition.name,
value: accessCondition.name
}
);
}
accessConditionTypeModelConfig.options = accessConditionTypeOptions;
// Dynamically assign of relation in config. For startdate, endDate, groups.
const hasStart = [];
const hasEnd = [];
const hasGroups = [];
this.availableAccessConditionOptions.forEach((condition) => {
const showStart: boolean = condition.hasStartDate === true;
const showEnd: boolean = condition.hasEndDate === true;
const showGroups: boolean = showStart || showEnd;
if (showStart) {
hasStart.push({ id: 'name', value: condition.name });
}
if (showEnd) {
hasEnd.push({ id: 'name', value: condition.name });
}
if (showGroups) {
hasGroups.push({ id: 'name', value: condition.name });
}
});
const confStart = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart }] };
const confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] };
accessConditionsArrayConfig.groupFactory = () => {
const type = new DynamicSelectModel(accessConditionTypeModelConfig, FORM_ACCESS_CONDITION_TYPE_LAYOUT);
const startDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart);
const endDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd);
const startDate = new DynamicDatePickerModel(startDateConfig, FORM_ACCESS_CONDITION_START_DATE_LAYOUT);
const endDate = new DynamicDatePickerModel(endDateConfig, FORM_ACCESS_CONDITION_END_DATE_LAYOUT);
const accessConditionGroupConfig = Object.assign({}, ACCESS_CONDITION_GROUP_CONFIG);
accessConditionGroupConfig.group = [type];
if (hasStart.length > 0) {
accessConditionGroupConfig.group.push(startDate);
}
if (hasEnd.length > 0) {
accessConditionGroupConfig.group.push(endDate);
}
return [new DynamicFormGroupModel(accessConditionGroupConfig, ACCESS_CONDITION_GROUP_LAYOUT)];
};
// Number of access conditions blocks in form
accessConditionsArrayConfig.initialCount = isNotEmpty(this.accessesData.accessConditions) ? this.accessesData.accessConditions.length : 1;
formModel.push(
new DynamicFormArrayModel(accessConditionsArrayConfig, ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT)
);
this.initModelData(formModel);
return formModel;
}
protected retrieveValueFromField(field: any) {
const temp = Array.isArray(field) ? field[0] : field;
return (temp) ? temp.value : undefined;
}
}

View File

@@ -0,0 +1,123 @@
import {
DynamicDatePickerModelConfig,
DynamicFormArrayModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicSelectModelConfig,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
import { DynamicCheckboxModelConfig } from '@ng-dynamic-forms/core/lib/model/checkbox/dynamic-checkbox.model';
export const ACCESS_FORM_CHECKBOX_CONFIG: DynamicCheckboxModelConfig = {
id: 'discoverable',
name: 'discoverable'
};
export const ACCESS_FORM_CHECKBOX_LAYOUT = {
element: {
container: 'custom-control custom-checkbox pl-1',
control: 'custom-control-input',
label: 'custom-control-label pt-1'
}
};
export const ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'accessConditionGroup',
group: []
};
export const ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'form-group access-condition-group col',
container: 'pl-1 pr-1',
control: 'form-row '
}
};
export const ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = {
id: 'accessCondition',
groupFactory: null,
};
export const ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = {
grid: {
group: 'form-row pt-4',
}
};
export const FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'name',
label: 'submission.sections.accesses.form.access-condition-label',
hint: 'submission.sections.accesses.form.access-condition-hint',
options: []
};
export const FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'col-12',
label: 'col-form-label name-label'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate',
label: 'submission.sections.accesses.form.from-label',
hint: 'submission.sections.accesses.form.from-hint',
placeholder: 'submission.sections.accesses.form.from-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.accesses.form.date-required-from'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'endDate',
label: 'submission.sections.accesses.form.until-label',
hint: 'submission.sections.accesses.form.until-hint',
placeholder: 'submission.sections.accesses.form.until-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.accesses.form.date-required-until'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { SubmissionState } from '../../submission.reducers';
import { isNotUndefined } from '../../../shared/empty.util';
import { submissionSectionDataFromIdSelector } from '../../selectors';
import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model';
/**
* A service that provides methods to handle submission item's accesses condition state.
*/
@Injectable()
export class SectionAccessesService {
/**
* Initialize service variables
*
* @param {Store<SubmissionState>} store
*/
constructor(private store: Store<SubmissionState>) { }
/**
* Return item's accesses condition state.
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @returns {Observable}
* Emits bitstream's metadata
*/
public getAccessesData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionAccessesObject> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
filter((state) => isNotUndefined(state)),
distinctUntilChanged());
}
}

View File

@@ -64,9 +64,9 @@ export class SubmissionSectionContainerComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.objectInjector = Injector.create({ this.objectInjector = Injector.create({
providers: [ providers: [
{provide: 'collectionIdProvider', useFactory: () => (this.collectionId), deps: []}, { provide: 'collectionIdProvider', useFactory: () => (this.collectionId), deps: [] },
{provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: []}, { provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: [] },
{provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: []}, { provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: [] },
], ],
parent: this.injector parent: this.injector
}); });

View File

@@ -12,7 +12,7 @@ import { SubmissionServiceStub } from '../../../shared/testing/submission-servic
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { SectionsService } from '../sections.service'; import { SectionsService } from '../sections.service';
import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub';
import { SubmissionSectionformComponent } from './section-form.component'; import { SubmissionSectionFormComponent } from './section-form.component';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock';
@@ -137,11 +137,11 @@ const dynamicFormControlEvent: DynamicFormControlEvent = {
type: DynamicFormControlEventType.Change type: DynamicFormControlEventType.Change
}; };
describe('SubmissionSectionformComponent test suite', () => { describe('SubmissionSectionFormComponent test suite', () => {
let comp: SubmissionSectionformComponent; let comp: SubmissionSectionFormComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<SubmissionSectionformComponent>; let fixture: ComponentFixture<SubmissionSectionFormComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let notificationsServiceStub: NotificationsServiceStub; let notificationsServiceStub: NotificationsServiceStub;
let formService: any = getMockFormService(); let formService: any = getMockFormService();
@@ -167,7 +167,7 @@ describe('SubmissionSectionformComponent test suite', () => {
], ],
declarations: [ declarations: [
FormComponent, FormComponent,
SubmissionSectionformComponent, SubmissionSectionFormComponent,
TestComponent TestComponent
], ],
providers: [ providers: [
@@ -186,7 +186,7 @@ describe('SubmissionSectionformComponent test suite', () => {
{ provide: 'submissionIdProvider', useValue: submissionId }, { provide: 'submissionIdProvider', useValue: submissionId },
{ provide: SubmissionObjectDataService, useValue: { getHrefByID: () => observableOf('testUrl'), findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem()) } }, { provide: SubmissionObjectDataService, useValue: { getHrefByID: () => observableOf('testUrl'), findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem()) } },
ChangeDetectorRef, ChangeDetectorRef,
SubmissionSectionformComponent SubmissionSectionFormComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -215,7 +215,7 @@ describe('SubmissionSectionformComponent test suite', () => {
testFixture.destroy(); testFixture.destroy();
}); });
it('should create SubmissionSectionformComponent', inject([SubmissionSectionformComponent], (app: SubmissionSectionformComponent) => { it('should create SubmissionSectionFormComponent', inject([SubmissionSectionFormComponent], (app: SubmissionSectionFormComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
@@ -224,7 +224,7 @@ describe('SubmissionSectionformComponent test suite', () => {
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SubmissionSectionformComponent); fixture = TestBed.createComponent(SubmissionSectionFormComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.inject(SubmissionService as any); submissionServiceStub = TestBed.inject(SubmissionService as any);

View File

@@ -44,7 +44,7 @@ import { RemoteData } from '../../../core/data/remote-data';
templateUrl: './section-form.component.html', templateUrl: './section-form.component.html',
}) })
@renderSectionFor(SectionsType.SubmissionForm) @renderSectionFor(SectionsType.SubmissionForm)
export class SubmissionSectionformComponent extends SectionModelComponent { export class SubmissionSectionFormComponent extends SectionModelComponent {
/** /**
* The form id * The form id

View File

@@ -4,5 +4,6 @@ export enum SectionsType {
Upload = 'upload', Upload = 'upload',
License = 'license', License = 'license',
CcLicense = 'cclicense', CcLicense = 'cclicense',
collection = 'collection' collection = 'collection',
AccessesCondition = 'accessCondition',
} }

View File

@@ -28,7 +28,7 @@ export const BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfi
export const BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = { export const BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = {
element: { element: {
host: 'form-group flex-fill access-condition-group', host: 'form-group access-condition-group col',
container: 'pl-1 pr-1', container: 'pl-1 pr-1',
control: 'form-row ' control: 'form-row '
} }
@@ -47,6 +47,7 @@ export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLa
export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig<any> = { export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'name', id: 'name',
label: 'submission.sections.upload.form.access-condition-label', label: 'submission.sections.upload.form.access-condition-label',
hint: 'submission.sections.upload.form.access-condition-hint',
options: [] options: []
}; };
export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = { export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = {
@@ -59,6 +60,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayo
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = { export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate', id: 'startDate',
label: 'submission.sections.upload.form.from-label', label: 'submission.sections.upload.form.from-label',
hint: 'submission.sections.upload.form.from-hint',
placeholder: 'submission.sections.upload.form.from-placeholder', placeholder: 'submission.sections.upload.form.from-placeholder',
inline: false, inline: false,
toggleIcon: 'far fa-calendar-alt', toggleIcon: 'far fa-calendar-alt',
@@ -89,6 +91,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormContr
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = { export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'endDate', id: 'endDate',
label: 'submission.sections.upload.form.until-label', label: 'submission.sections.upload.form.until-label',
hint: 'submission.sections.upload.form.until-hint',
placeholder: 'submission.sections.upload.form.until-placeholder', placeholder: 'submission.sections.upload.form.until-placeholder',
inline: false, inline: false,
toggleIcon: 'far fa-calendar-alt', toggleIcon: 'far fa-calendar-alt',

View File

@@ -69,3 +69,4 @@ export function submissionSectionServerErrorsFromIdSelector(submissionId: string
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId); const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'serverValidationErrors'); return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'serverValidationErrors');
} }

View File

@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CoreModule } from '../core/core.module'; import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SubmissionSectionformComponent } from './sections/form/section-form.component'; import { SubmissionSectionFormComponent } from './sections/form/section-form.component';
import { SectionsDirective } from './sections/sections.directive'; import { SectionsDirective } from './sections/sections.directive';
import { SectionsService } from './sections/sections.service'; import { SectionsService } from './sections/sections.service';
import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component'; import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component';
@@ -38,14 +38,23 @@ import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.com
import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component'; import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component';
import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component'; import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component';
import { FormModule } from '../shared/form/form.module'; import { FormModule } from '../shared/form/form.module';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAccordionModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component';
import { SubmissionAccessesConfigService } from '../core/config/submission-accesses-config.service';
import { SectionAccessesService } from './sections/accesses/section-accesses.service';
const DECLARATIONS = [ const ENTRY_COMPONENTS = [
SubmissionSectionUploadAccessConditionsComponent, // put only entry components that use custom decorator
SubmissionSectionUploadComponent, SubmissionSectionUploadComponent,
SubmissionSectionformComponent, SubmissionSectionFormComponent,
SubmissionSectionLicenseComponent, SubmissionSectionLicenseComponent,
SubmissionSectionCcLicensesComponent, SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent,
SubmissionSectionUploadFileEditComponent
];
const DECLARATIONS = [
...ENTRY_COMPONENTS,
SectionsDirective, SectionsDirective,
SubmissionEditComponent, SubmissionEditComponent,
ThemedSubmissionEditComponent, ThemedSubmissionEditComponent,
@@ -57,6 +66,7 @@ const DECLARATIONS = [
ThemedSubmissionSubmitComponent, ThemedSubmissionSubmitComponent,
SubmissionUploadFilesComponent, SubmissionUploadFilesComponent,
SubmissionSectionContainerComponent, SubmissionSectionContainerComponent,
SubmissionSectionUploadAccessConditionsComponent,
SubmissionSectionUploadFileComponent, SubmissionSectionUploadFileComponent,
SubmissionSectionUploadFileEditComponent, SubmissionSectionUploadFileEditComponent,
SubmissionSectionUploadFileViewComponent, SubmissionSectionUploadFileViewComponent,
@@ -64,14 +74,7 @@ const DECLARATIONS = [
ThemedSubmissionImportExternalComponent, ThemedSubmissionImportExternalComponent,
SubmissionImportExternalSearchbarComponent, SubmissionImportExternalSearchbarComponent,
SubmissionImportExternalPreviewComponent, SubmissionImportExternalPreviewComponent,
SubmissionImportExternalCollectionComponent SubmissionImportExternalCollectionComponent,
];
const ENTRY_COMPONENTS = [
SubmissionSectionUploadComponent,
SubmissionSectionformComponent,
SubmissionSectionLicenseComponent,
SubmissionSectionCcLicensesComponent
]; ];
@NgModule({ @NgModule({
@@ -84,14 +87,17 @@ const ENTRY_COMPONENTS = [
JournalEntitiesModule.withEntryComponents(), JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(),
FormModule, FormModule,
NgbAccordionModule NgbAccordionModule,
NgbModalModule
], ],
declarations: DECLARATIONS, declarations: DECLARATIONS,
exports: DECLARATIONS, exports: DECLARATIONS,
providers: [ providers: [
SectionUploadService, SectionUploadService,
SectionsService, SectionsService,
SubmissionUploadsConfigService SubmissionUploadsConfigService,
SubmissionAccessesConfigService,
SectionAccessesService
] ]
}) })
@@ -106,7 +112,7 @@ export class SubmissionModule {
static withEntryComponents() { static withEntryComponents() {
return { return {
ngModule: SubmissionModule, ngModule: SubmissionModule,
providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) providers: ENTRY_COMPONENTS.map((component) => ({ provide: component }))
}; };
} }
} }

View File

@@ -3836,6 +3836,8 @@
"submission.sections.submit.progressbar.accessCondition": "Item access conditions",
"submission.sections.submit.progressbar.CClicense": "Creative commons license", "submission.sections.submit.progressbar.CClicense": "Creative commons license",
"submission.sections.submit.progressbar.describe.recycle": "Recycle", "submission.sections.submit.progressbar.describe.recycle": "Recycle",
@@ -3892,6 +3894,8 @@
"submission.sections.upload.form.access-condition-label": "Access condition type", "submission.sections.upload.form.access-condition-label": "Access condition type",
"submission.sections.upload.form.access-condition-hint": "Select an access condition to apply on the bitstream once the item is deposited",
"submission.sections.upload.form.date-required": "Date is required.", "submission.sections.upload.form.date-required": "Date is required.",
"submission.sections.upload.form.date-required-from": "Grant access from date is required.", "submission.sections.upload.form.date-required-from": "Grant access from date is required.",
@@ -3900,6 +3904,8 @@
"submission.sections.upload.form.from-label": "Grant access from", "submission.sections.upload.form.from-label": "Grant access from",
"submission.sections.upload.form.from-hint": "Select the date from which the related access condition is applied",
"submission.sections.upload.form.from-placeholder": "From", "submission.sections.upload.form.from-placeholder": "From",
"submission.sections.upload.form.group-label": "Group", "submission.sections.upload.form.group-label": "Group",
@@ -3908,6 +3914,8 @@
"submission.sections.upload.form.until-label": "Grant access until", "submission.sections.upload.form.until-label": "Grant access until",
"submission.sections.upload.form.until-hint": "Select the date until which the related access condition is applied",
"submission.sections.upload.form.until-placeholder": "Until", "submission.sections.upload.form.until-placeholder": "Until",
"submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):",
@@ -3928,6 +3936,35 @@
"submission.sections.upload.upload-successful": "Upload successful", "submission.sections.upload.upload-successful": "Upload successful",
"submission.sections.accesses.form.discoverable-description": "When checked, this item will be discoverable in search/browse. When unchecked, the item will only be available via a direct link and will never appear in search/browse.",
"submission.sections.accesses.form.discoverable-label": "Discoverable",
"submission.sections.accesses.form.access-condition-label": "Access condition type",
"submission.sections.accesses.form.access-condition-hint": "Select an access condition to apply on the item once it is deposited",
"submission.sections.accesses.form.date-required": "Date is required.",
"submission.sections.accesses.form.date-required-from": "Grant access from date is required.",
"submission.sections.accesses.form.date-required-until": "Grant access until date is required.",
"submission.sections.accesses.form.from-label": "Grant access from",
"submission.sections.accesses.form.from-hint": "Select the date from which the related access condition is applied",
"submission.sections.accesses.form.from-placeholder": "From",
"submission.sections.accesses.form.group-label": "Group",
"submission.sections.accesses.form.group-required": "Group is required.",
"submission.sections.accesses.form.until-label": "Grant access until",
"submission.sections.accesses.form.until-hint": "Select the date until which the related access condition is applied",
"submission.sections.accesses.form.until-placeholder": "Until",
"submission.submit.breadcrumbs": "New submission", "submission.submit.breadcrumbs": "New submission",