diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index accd0f23a5..6868b8d546 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -462,7 +462,7 @@ describe('CommunityListService', () => { }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { - beforeAll((done) => { + beforeEach((done) => { service.transformCommunity(communityWithSubcoms, 0, null, null) .pipe(take(1)) .subscribe((value) => { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8c990ae0b1..07ff1f9572 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -148,6 +148,10 @@ import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; import { TokenResponseParsingService } from './auth/token-response-parsing.service'; +import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; +import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; +import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; +import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -214,6 +218,8 @@ const PROVIDERS = [ BrowseItemsResponseParsingService, BrowseService, ConfigResponseParsingService, + SubmissionCcLicenseDataService, + SubmissionCcLicenseUrlDataService, SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionRestService, @@ -300,6 +306,8 @@ export const models = License, WorkflowItem, WorkspaceItem, + SubmissionCcLicence, + SubmissionCcLicenceUrl, SubmissionDefinitionsModel, SubmissionFormsModel, SubmissionSectionModel, diff --git a/src/app/core/shared/hal-resource.model.ts b/src/app/core/shared/hal-resource.model.ts index b6ef822a23..334509007b 100644 --- a/src/app/core/shared/hal-resource.model.ts +++ b/src/app/core/shared/hal-resource.model.ts @@ -1,4 +1,5 @@ import { HALLink } from './hal-link.model'; +import { deserialize } from 'cerialize'; /** * Represents HAL resources. @@ -6,10 +7,13 @@ import { HALLink } from './hal-link.model'; * A HAL resource has a _links section with at least a self link. */ export class HALResource { + /** * The {@link HALLink}s for this {@link HALResource} */ + @deserialize _links: { + /** * The {@link HALLink} that refers to this {@link HALResource} */ diff --git a/src/app/core/submission/models/submission-cc-licence-link.resource-type.ts b/src/app/core/submission/models/submission-cc-licence-link.resource-type.ts new file mode 100644 index 0000000000..b4e38970f0 --- /dev/null +++ b/src/app/core/submission/models/submission-cc-licence-link.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for License + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUBMISSION_CC_LICENSE_URL = new ResourceType('submissioncclicenseUrl'); diff --git a/src/app/core/submission/models/submission-cc-licence.resource-type.ts b/src/app/core/submission/models/submission-cc-licence.resource-type.ts new file mode 100644 index 0000000000..a15b8a0c66 --- /dev/null +++ b/src/app/core/submission/models/submission-cc-licence.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for License + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUBMISSION_CC_LICENSE = new ResourceType('submissioncclicense'); diff --git a/src/app/core/submission/models/submission-cc-license-url.model.ts b/src/app/core/submission/models/submission-cc-license-url.model.ts new file mode 100644 index 0000000000..a7c933ecb1 --- /dev/null +++ b/src/app/core/submission/models/submission-cc-license-url.model.ts @@ -0,0 +1,23 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { HALResource } from '../../shared/hal-resource.model'; +import { SUBMISSION_CC_LICENSE_URL } from './submission-cc-licence-link.resource-type'; + +@typedObject +@inheritSerialization(HALResource) +export class SubmissionCcLicenceUrl extends HALResource { + + static type = SUBMISSION_CC_LICENSE_URL; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + url: string; +} diff --git a/src/app/core/submission/models/submission-cc-license.model.ts b/src/app/core/submission/models/submission-cc-license.model.ts new file mode 100644 index 0000000000..92c48fdbfe --- /dev/null +++ b/src/app/core/submission/models/submission-cc-license.model.ts @@ -0,0 +1,42 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { HALResource } from '../../shared/hal-resource.model'; +import { SUBMISSION_CC_LICENSE } from './submission-cc-licence.resource-type'; + +@typedObject +@inheritSerialization(HALResource) +export class SubmissionCcLicence extends HALResource { + + static type = SUBMISSION_CC_LICENSE; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @autoserialize + name: string; + + @autoserialize + fields: Field[]; +} + +export interface Field { + id: string; + label: string; + description: string; + enums: Option[]; +} + +export interface Option { + id: string; + label: string; + description: string; +} diff --git a/src/app/core/submission/models/workspaceitem-section-cc-license.model.ts b/src/app/core/submission/models/workspaceitem-section-cc-license.model.ts new file mode 100644 index 0000000000..ed7d68354c --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-cc-license.model.ts @@ -0,0 +1,15 @@ +import { Option } from './submission-cc-license.model'; + +/** + * An interface to represent the submission's creative commons license section data. + */ +export interface WorkspaceitemSectionCcLicenseObject { + ccLicense?: { + id: string; + fields: { + [fieldId: string]: Option; + } + }; + uri?: string; + accepted?: boolean; +} diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index 165e69869c..6ff756a323 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -1,6 +1,7 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; +import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model'; /** * An interface to represent submission's section object. @@ -17,4 +18,5 @@ export type WorkspaceitemSectionDataType = WorkspaceitemSectionUploadObject | WorkspaceitemSectionFormObject | WorkspaceitemSectionLicenseObject + | WorkspaceitemSectionCcLicenseObject | string; diff --git a/src/app/core/submission/submission-cc-license-data.service.ts b/src/app/core/submission/submission-cc-license-data.service.ts new file mode 100644 index 0000000000..5a3fa1ec2b --- /dev/null +++ b/src/app/core/submission/submission-cc-license-data.service.ts @@ -0,0 +1,34 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type'; +import { SubmissionCcLicence } from './models/submission-cc-license.model'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; + +@Injectable() +@dataService(SUBMISSION_CC_LICENSE) +export class SubmissionCcLicenseDataService extends DataService { + + protected linkPath = 'submissioncclicenses'; + + constructor( + protected comparator: DefaultChangeAnalyzer, + protected halService: HALEndpointService, + protected http: HttpClient, + protected notificationsService: NotificationsService, + protected objectCache: ObjectCacheService, + protected rdbService: RemoteDataBuildService, + protected requestService: RequestService, + protected store: Store, + ) { + super(); + } +} diff --git a/src/app/core/submission/submission-cc-license-url-data.service.ts b/src/app/core/submission/submission-cc-license-url-data.service.ts new file mode 100644 index 0000000000..0ec40e1403 --- /dev/null +++ b/src/app/core/submission/submission-cc-license-url-data.service.ts @@ -0,0 +1,76 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { SubmissionCcLicenceUrl } from './models/submission-cc-license-url.model'; +import { SUBMISSION_CC_LICENSE_URL } from './models/submission-cc-licence-link.resource-type'; +import { Field, Option, SubmissionCcLicence } from './models/submission-cc-license.model'; +import { Observable } from 'rxjs'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; +import { isNotEmpty } from '../../shared/empty.util'; + +@Injectable() +@dataService(SUBMISSION_CC_LICENSE_URL) +export class SubmissionCcLicenseUrlDataService extends DataService { + + protected linkPath = 'submissioncclicenseUrl-search'; + + constructor( + protected comparator: DefaultChangeAnalyzer, + protected halService: HALEndpointService, + protected http: HttpClient, + protected notificationsService: NotificationsService, + protected objectCache: ObjectCacheService, + protected rdbService: RemoteDataBuildService, + protected requestService: RequestService, + protected store: Store, + ) { + super(); + } + + /** + * Get the link to the Creative Commons license corresponding to the given type and options. + * @param ccLicense the Creative Commons license type + * @param options the selected options of the license fields + */ + getCcLicenseLink(ccLicense: SubmissionCcLicence, options: Map): Observable { + + return this.getSearchByHref( + 'rightsByQuestions',{ + searchParams: [ + { + fieldName: 'license', + fieldValue: ccLicense.id + }, + ...ccLicense.fields.map( + (field) => { + return { + fieldName: `answer_${field.id}`, + fieldValue: options.get(field).id, + } + }), + ] + } + ).pipe( + switchMap((href) => this.findByHref(href)), + getSucceededRemoteData(), + getRemoteDataPayload(), + map((response) => response.url), + ); + } + + protected getSearchEndpoint(searchMethod: string): Observable { + return this.halService.getEndpoint(`${this.linkPath}`).pipe( + filter((href: string) => isNotEmpty(href)), + map((href: string) => `${href}/${searchMethod}`)); + } +} diff --git a/src/app/shared/ds-select/ds-select.component.html b/src/app/shared/ds-select/ds-select.component.html new file mode 100644 index 0000000000..5a15155d4f --- /dev/null +++ b/src/app/shared/ds-select/ds-select.component.html @@ -0,0 +1,31 @@ +
+ +
+ +
+ + {{ label | translate }} + +
+ + + + +
+ +
diff --git a/src/app/shared/ds-select/ds-select.component.scss b/src/app/shared/ds-select/ds-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/ds-select/ds-select.component.spec.ts b/src/app/shared/ds-select/ds-select.component.spec.ts new file mode 100644 index 0000000000..ddec73348c --- /dev/null +++ b/src/app/shared/ds-select/ds-select.component.spec.ts @@ -0,0 +1,30 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DsSelectComponent } from './ds-select.component'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('DsSelectComponent', () => { + let component: DsSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [ + DsSelectComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/ds-select/ds-select.component.ts b/src/app/shared/ds-select/ds-select.component.ts new file mode 100644 index 0000000000..26d1560c01 --- /dev/null +++ b/src/app/shared/ds-select/ds-select.component.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +/** + * Component which represent a DSpace dropdown selector. + */ +@Component({ + selector: 'ds-select', + templateUrl: './ds-select.component.html', + styleUrls: ['./ds-select.component.scss'] +}) +export class DsSelectComponent { + + /** + * An optional label for the dropdown selector. + */ + @Input() + label: string; + + /** + * Whether the dropdown selector is disabled. + */ + @Input() + disabled: boolean; + + /** + * Emits an event when the dropdown selector is opened or closed. + */ + @Output() + toggled = new EventEmitter(); + + /** + * Emits an event when the dropdown selector or closed. + */ + @Output() + close = new EventEmitter(); +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 98526bf2bc..cc61a6d868 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -206,6 +206,7 @@ import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-grou import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; +import { DsSelectComponent } from './ds-select/ds-select.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -282,6 +283,7 @@ const COMPONENTS = [ DsDynamicFormGroupComponent, DsDynamicFormArrayComponent, DsDatePickerInlineComponent, + DsSelectComponent, ErrorComponent, FormComponent, LangSwitchComponent, diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html new file mode 100644 index 0000000000..20743540a8 --- /dev/null +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html @@ -0,0 +1,144 @@ +
+ + + + + + + + {{ getSelectedCcLicense().name }} + + + + {{ 'submission.sections.ccLicense.change' | translate }} + + + {{ 'submission.sections.ccLicense.select' | translate }} + + + + + + + + + + +
+ + + +
+ +
+
+ {{ field.label }} +
+ +
+ + + +
+ + + + + +
+ +
+ + + + + {{ option.label }} + + + {{ 'submission.sections.ccLicense.option.select' | translate }} + + + +
+ +
+
+
+ + +
+
+ + {{ option.label }} +
+
+
+ +
+ +
+ + + +
+ +
+
+
+ {{ 'submission.sections.ccLicense.link' | translate }} +
+ + {{ licenseLink }} + +
+
+ + {{ 'submission.sections.ccLicense.confirmation' | translate }} +
+
+
+
+
diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss new file mode 100644 index 0000000000..62a902b79a --- /dev/null +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss @@ -0,0 +1,3 @@ +.options-select-menu { + max-height: 25vh; +} diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts new file mode 100644 index 0000000000..230c72b522 --- /dev/null +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -0,0 +1,280 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SubmissionSectionCcLicensesComponent } from './submission-section-cc-licenses.component'; +import { SUBMISSION_CC_LICENSE } from '../../../core/submission/models/submission-cc-licence.resource-type'; +import { of as observableOf } from 'rxjs'; +import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { SharedModule } from '../../../shared/shared.module'; +import { SectionsService } from '../sections.service'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsType } from '../sections-type'; +import { RemoteData } from '../../../core/data/remote-data'; +import { TranslateModule } from '@ngx-translate/core'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; +import { cold } from 'jasmine-marbles'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; + +describe('SubmissionSectionCcLicensesComponent', () => { + + let component: SubmissionSectionCcLicensesComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + const sectionObject: SectionDataObject = { + config: 'test config', + mandatory: true, + data: {}, + errors: [], + header: 'test header', + id: 'test section id', + sectionType: SectionsType.SubmissionForm + }; + + const submissionCcLicenses: SubmissionCcLicence[] = [ + { + id: 'test license id 1', + type: SUBMISSION_CC_LICENSE, + name: 'test license name 1', + fields: [ + { + id: 'test-field-id-1a', + label: 'test field label 1a', + description: 'test field description 1a', + enums: [ + { + id: 'test enum id 1a I', + label: 'test enum label 1a I', + description: 'test enum description 1a I', + }, + { + id: 'test enum id 1a II', + label: 'test enum label 1a II', + description: 'test enum description 1a II', + }, + ], + }, + { + id: 'test-field-id-1b', + label: 'test field label 1b', + description: 'test field description 1b', + enums: [ + { + id: 'test enum id 1b I', + label: 'test enum label 1b I', + description: 'test enum description 1b I', + }, + { + id: 'test enum id 1b II', + label: 'test enum label 1b II', + description: 'test enum description 1b II', + }, + ], + }, + ], + _links: { + self: { + href: 'test link', + }, + }, + }, + { + id: 'test license id 2', + type: SUBMISSION_CC_LICENSE, + name: 'test license name 2', + fields: [ + { + id: 'test-field-id-2a', + label: 'test field label 2a', + description: 'test field description 2a', + enums: [ + { + id: 'test enum id 2a I', + label: 'test enum label 2a I', + description: 'test enum description 2a I' + }, + { + id: 'test enum id 2a II', + label: 'test enum label 2a II', + description: 'test enum description 2a II' + }, + ], + }, + { + id: 'test-field-id-2b', + label: 'test field label 2b', + description: 'test field description 2b', + enums: [ + { + id: 'test enum id 2b I', + label: 'test enum label 2b I', + description: 'test enum description 2b I' + }, + { + id: 'test enum id 2b II', + label: 'test enum label 2b II', + description: 'test enum description 2b II' + }, + ], + }, + ], + _links: { + self: { + href: 'test link', + }, + }, + }, + ]; + + const submissionCcLicensesDataService = jasmine.createSpyObj('submissionCcLicensesDataService', { + findAll: observableOf(new RemoteData( + false, + false, + true, + undefined, + new PaginatedList(new PageInfo(), submissionCcLicenses), + )), + }); + + const submissionCcLicenseUrlDataService = jasmine.createSpyObj('submissionCcLicenseUrlDataService', { + getCcLicenseLink: observableOf(new RemoteData( + false, + false, + true, + undefined, + { + url: 'test cc license link', + } + )), + }); + + const sectionService = { + getSectionState: () => { + return observableOf({}); + }, + setSectionStatus: () => undefined, + updateSectionData: (submissionId, sectionId, updatedData) => { + component.sectionData.data = updatedData; + } + }; + + const operationsBuilder = jasmine.createSpyObj('operationsBuilder', { + add: undefined, + remove: undefined, + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + TranslateModule.forRoot(), + ], + declarations: [ + SubmissionSectionCcLicensesComponent, + ], + providers: [ + { provide: SubmissionCcLicenseDataService, useValue: submissionCcLicensesDataService }, + { provide: SubmissionCcLicenseUrlDataService, useValue: submissionCcLicenseUrlDataService }, + { provide: SectionsService, useValue: sectionService }, + { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, + { provide: 'collectionIdProvider', useValue: 'test collection id' }, + { provide: 'sectionDataProvider', useValue: sectionObject }, + { provide: 'submissionIdProvider', useValue: 'test submission id' }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionSectionCcLicensesComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should display a dropdown with the different cc licenses', () => { + expect( + de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(1)')).nativeElement.innerText + ).toContain('test license name 1'); + expect( + de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText + ).toContain('test license name 2'); + }); + + describe('when a license is selected', () => { + + const ccLicence = submissionCcLicenses[1]; + + beforeEach(() => { + component.selectCcLicense(ccLicence); + fixture.detectChanges(); + }); + + it('should display the selected cc license', () => { + expect( + de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText + ).toContain('test license name 2'); + }); + + it('should display all field labels of the selected cc license only', () => { + expect(de.query(By.css('div.test-field-id-1a'))).toBeNull(); + expect(de.query(By.css('div.test-field-id-1b'))).toBeNull(); + expect(de.query(By.css('div.test-field-id-2a'))).toBeTruthy(); + expect(de.query(By.css('div.test-field-id-2b'))).toBeTruthy(); + }); + + it('should not display a cc license link', () => { + expect(de.query(By.css('.license-link'))).toBeNull(); + }); + + it('should have section status incomplete', () => { + expect(component.getSectionStatus()).toBeObservable(cold('(a|)', { a: false })); + }); + + describe('when all options have a value selected', () => { + + beforeEach(() => { + component.selectOption(ccLicence, ccLicence.fields[0], ccLicence.fields[0].enums[1]); + component.selectOption(ccLicence, ccLicence.fields[1], ccLicence.fields[1].enums[0]); + fixture.detectChanges(); + }); + + it('should call the submission cc licenses data service getCcLicenseLink method', () => { + expect(submissionCcLicenseUrlDataService.getCcLicenseLink).toHaveBeenCalledWith( + ccLicence, + new Map([ + [ccLicence.fields[0], ccLicence.fields[0].enums[1]], + [ccLicence.fields[1], ccLicence.fields[1].enums[0]], + ]) + ); + }); + + it('should display a cc license link', () => { + expect(de.query(By.css('.license-link'))).toBeTruthy(); + }); + + it('should not be accepted', () => { + expect(component.accepted).toBeFalse(); + }); + + it('should have section status incomplete', () => { + expect(component.getSectionStatus()).toBeObservable(cold('(a|)', { a: false })); + }); + + describe('when the cc license is accepted', () => { + + beforeEach(() => { + component.setAccepted(true); + fixture.detectChanges(); + }); + + it('should have section status complete', () => { + expect(component.getSectionStatus()).toBeObservable(cold('(a|)', { a: true })); + }); + }); + }); + }); +}); diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts new file mode 100644 index 0000000000..d455bd5e22 --- /dev/null +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts @@ -0,0 +1,279 @@ +import { Component, Inject } from '@angular/core'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; +import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { SectionModelComponent } from '../models/section.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { WorkspaceitemSectionCcLicenseObject } from '../../../core/submission/models/workspaceitem-section-cc-license.model'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; + +/** + * This component represents the submission section to select the Creative Commons license. + */ +@Component({ + selector: 'ds-submission-section-cc-licenses', + templateUrl: './submission-section-cc-licenses.component.html', + styleUrls: ['./submission-section-cc-licenses.component.scss'] +}) +@renderSectionFor(SectionsType.CcLicense) +export class SubmissionSectionCcLicensesComponent extends SectionModelComponent { + + /** + * The form id + * @type {string} + */ + public formId: string; + + /** + * A boolean representing if this section is loading + * @type {boolean} + */ + public isLoading = true; + + /** + * The [JsonPatchOperationPathCombiner] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + + /** + * The list of Subscriptions this component subscribes to. + */ + private subscriptions: Subscription[] = []; + + /** + * Cache of the available Creative Commons licenses. + */ + submissionCcLicenses: SubmissionCcLicence[]; + + /** + * Reference to NgbModal + */ + protected modalRef: NgbModalRef; + + /** + * The Creative Commons link saved in the workspace item. + */ + get storedCcLicenseLink(): string { + return this.data.uri; + } + + /** + * The accepted state for the selected Creative Commons license. + */ + get accepted(): boolean { + if (this.data.accepted === undefined) { + return !!this.data.uri; + } + return this.data.accepted; + } + + constructor( + protected modalService: NgbModal, + protected sectionService: SectionsService, + protected submissionCcLicensesDataService: SubmissionCcLicenseDataService, + protected submissionCcLicenseUrlDataService: SubmissionCcLicenseUrlDataService, + protected operationsBuilder: JsonPatchOperationsBuilder, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string + ) { + super( + injectedCollectionId, + injectedSectionData, + injectedSubmissionId, + ); + } + + /** + * The data of this section. + */ + get data(): WorkspaceitemSectionCcLicenseObject { + return this.sectionData.data as WorkspaceitemSectionCcLicenseObject; + } + + /** + * Select a given Creative Commons license. + * @param ccLicense the Creative Commons license to select. + */ + selectCcLicense(ccLicense: SubmissionCcLicence) { + if (!!this.getSelectedCcLicense() && this.getSelectedCcLicense().id === ccLicense.id) { + return; + } + this.setAccepted(false); + this.updateSectionData({ + ccLicense: { + id: ccLicense.id, + fields: {}, + }, + uri: undefined, + }); + } + + /** + * Get the selected Creative Commons license. + */ + getSelectedCcLicense(): SubmissionCcLicence { + if (!this.submissionCcLicenses || !this.data.ccLicense) { + return null; + } + return this.submissionCcLicenses.filter((ccLicense) => ccLicense.id === this.data.ccLicense.id)[0]; + } + + /** + * Select an option for a given license field. + * @param ccLicense the related Creative Commons license. + * @param field the field for which to select an option. + * @param option the option to select. + */ + selectOption(ccLicense: SubmissionCcLicence, field: Field, option: Option) { + if (this.isSelectedOption(ccLicense, field, option)) { + return; + } + this.updateSectionData({ + ccLicense: { + id: ccLicense.id, + fields: Object.assign({}, this.data.ccLicense.fields, { + [field.id]: option + }), + }, + accepted: false, + }); + } + + /** + * Get the selected option for a given license field. + * @param ccLicense the related Creative Commons license. + * @param field the field for which to get the selected option value. + */ + getSelectedOption(ccLicense: SubmissionCcLicence, field: Field): Option { + return this.data.ccLicense.fields[field.id]; + } + + /** + * Whether a given option is selected for a given Creative Commons license field. + * @param ccLicense the related Creative Commons license. + * @param field the field for which to check whether the option is selected. + * @param option the option for which to check whether it is selected. + */ + isSelectedOption(ccLicense: SubmissionCcLicence, field: Field, option: Option): boolean { + return this.getSelectedOption(ccLicense, field) && this.getSelectedOption(ccLicense, field).id === option.id; + } + + /** + * Get the link to the Creative Commons license corresponding with the selected options. + */ + getCcLicenseLink$(): Observable { + + if (!!this.storedCcLicenseLink) { + return observableOf(this.storedCcLicenseLink); + } + if (!this.getSelectedCcLicense() || this.getSelectedCcLicense().fields.some( + (field) => !this.getSelectedOption(this.getSelectedCcLicense(), field))) { + return undefined; + } + const selectedCcLicense = this.getSelectedCcLicense(); + return this.submissionCcLicenseUrlDataService.getCcLicenseLink( + selectedCcLicense, + new Map(selectedCcLicense.fields.map( + (field) => [field, this.getSelectedOption(selectedCcLicense, field)] + )), + ); + } + + /** + * Open a given info modal. + * @param content the modal content. + */ + openInfoModal(content) { + this.modalRef = this.modalService.open(content); + } + + /** + * Close the info modal. + */ + closeInfoModal() { + this.modalRef.close(); + } + + /** + * Get section status + * + * @return Observable + * the section status + */ + getSectionStatus(): Observable { + return observableOf(this.accepted); + } + + /** + * Unsubscribe from all subscriptions + */ + onSectionDestroy(): void { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + } + + /** + * Initialize the section. + */ + onSectionInit(): void { + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + this.subscriptions.push( + this.sectionService.getSectionState(this.submissionId, this.sectionData.id).pipe( + filter((sectionState) => { + return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)) + }), + distinctUntilChanged(), + map((sectionState) => sectionState.data as WorkspaceitemSectionCcLicenseObject), + ).subscribe((data) => { + if (this.data.accepted !== data.accepted) { + const path = this.pathCombiner.getPath('uri'); + if (data.accepted) { + this.getCcLicenseLink$().pipe( + take(1), + ).subscribe((link) => { + this.operationsBuilder.add(path, link.toString(), false, true); + }); + } else if (!!this.data.uri) { + this.operationsBuilder.remove(path); + } + } + this.sectionData.data = data; + }), + this.submissionCcLicensesDataService.findAll({elementsPerPage: Number.MAX_SAFE_INTEGER}).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((list) => list.page), + ).subscribe( + (licenses) => this.submissionCcLicenses = licenses + ), + ); + } + + /** + * Set the accepted state for the Creative Commons license. + * @param accepted the accepted state for the cc license. + */ + setAccepted(accepted: boolean) { + this.updateSectionData({ + accepted + }); + this.updateSectionStatus(); + } + + /** + * Update the section data for this section. + */ + updateSectionData(data: WorkspaceitemSectionCcLicenseObject) { + this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, Object.assign({}, this.data, data)); + } +} diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 806258baa8..93bf06b193 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -28,7 +28,7 @@ import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSubmitComponent } from './submit/submission-submit.component'; import { storeModuleConfig } from '../app.reducer'; -import { CoreState } from '../core/core.reducers'; +import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; @NgModule({ imports: [ @@ -44,8 +44,8 @@ import { CoreState } from '../core/core.reducers'; SubmissionSectionUploadComponent, SubmissionSectionformComponent, SubmissionSectionLicenseComponent, + SubmissionSectionCcLicensesComponent, SectionsDirective, - SubmissionSectionContainerComponent, SubmissionEditComponent, SubmissionFormSectionAddComponent, SubmissionFormCollectionComponent, @@ -53,6 +53,7 @@ import { CoreState } from '../core/core.reducers'; SubmissionFormFooterComponent, SubmissionSubmitComponent, SubmissionUploadFilesComponent, + SubmissionSectionContainerComponent, SubmissionSectionUploadFileComponent, SubmissionSectionUploadFileEditComponent, SubmissionSectionUploadFileViewComponent @@ -61,7 +62,9 @@ import { CoreState } from '../core/core.reducers'; SubmissionSectionUploadComponent, SubmissionSectionformComponent, SubmissionSectionLicenseComponent, - SubmissionSectionContainerComponent], + SubmissionSectionContainerComponent, + SubmissionSectionCcLicensesComponent, + ], exports: [ SubmissionEditComponent, SubmissionFormComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 9f3d47dcde..fd1c1fcbf8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2768,6 +2768,20 @@ "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", + "submission.sections.ccLicense.type": "License Type", + + "submission.sections.ccLicense.select": "Select a license type…", + + "submission.sections.ccLicense.change": "Change your license type…", + + "submission.sections.ccLicense.none": "No licenses available", + + "submission.sections.ccLicense.option.select": "Select an option…", + + "submission.sections.ccLicense.link": "You’ve selected the following license:", + + "submission.sections.ccLicense.confirmation": "I grant the license above", + "submission.sections.general.add-more": "Add more", "submission.sections.general.collection": "Collection", @@ -2798,7 +2812,7 @@ - "submission.sections.submit.progressbar.cclicense": "Creative commons license", + "submission.sections.submit.progressbar.CClicense": "Creative commons license", "submission.sections.submit.progressbar.describe.recycle": "Recycle",