diff --git a/src/app/core/helpers/index.ts b/src/app/core/helpers/index.ts index 71cd6afca..8a4ee14e8 100644 --- a/src/app/core/helpers/index.ts +++ b/src/app/core/helpers/index.ts @@ -1,4 +1,3 @@ export * from './http.helper'; export * from './i18n.helper'; -export * from './link-validator.helper'; export * from './types.helper'; diff --git a/src/app/core/helpers/link-validator.helper.ts b/src/app/core/helpers/link-validator.helper.ts deleted file mode 100644 index 03052cf5e..000000000 --- a/src/app/core/helpers/link-validator.helper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; - -export function linkValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value = control.value; - if (!value) { - return null; - } - - const urlPattern = /^(https):\/\/.+/i; - - const isValid = urlPattern.test(value); - - return isValid ? null : { link: true }; - }; -} diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.html b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.html new file mode 100644 index 000000000..87227b291 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.html @@ -0,0 +1,17 @@ +
+
+ @let formArrayControls = formArray().controls; + @for (control of formArrayControls; track $index) { +
+ + @if (formArrayControls.length > 1) { + + } +
+ } +
+ +
+ +
+
diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.scss b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts new file mode 100644 index 000000000..b29274281 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ArrayInputComponent } from './array-input.component'; + +describe('ArrayInputComponent', () => { + let component: ArrayInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArrayInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ArrayInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.ts b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.ts new file mode 100644 index 000000000..a65ad89f2 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.ts @@ -0,0 +1,34 @@ +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { FormArray, FormControl, ReactiveFormsModule, ValidatorFn } from '@angular/forms'; + +import { TextInputComponent } from '@shared/components'; + +@Component({ + selector: 'osf-array-input', + imports: [ReactiveFormsModule, Button, TextInputComponent], + templateUrl: './array-input.component.html', + styleUrl: './array-input.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ArrayInputComponent { + formArray = input.required>(); + inputPlaceholder = input.required(); + validators = input.required(); + + add() { + this.formArray().push( + new FormControl('', { + nonNullable: true, + validators: this.validators(), + }) + ); + } + + remove(index: number) { + if (this.formArray().length > 1) { + this.formArray().removeAt(index); + } + } +} diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html new file mode 100644 index 000000000..dd7e22bb7 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html @@ -0,0 +1,238 @@ +

Author Assertions

+ + +
+

Conflict of Interest

+ +

+ The Conflict of Interest (COI) assertion is made on behalf of all the authors listed for this preprint. COIs + include: financial involvement in any entity such as honoraria, grants, speaking fees, employment, consultancies, + stock ownership, expert testimony, and patents or licenses. COIs can also include non-financial interests such as + personal or professional relationships or pre-existing beliefs in the subject matter or materials discussed in + this preprint. +

+ +
+
+ + +
+ +
+ + +
+
+ + + @let coiStatementControl = authorAssertionsForm.controls['coiStatement']; + @if (coiStatementControl.errors?.['required'] && (coiStatementControl.touched || coiStatementControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } +
+
+ + +
+

Public Data

+ +

+ Data refers to raw and/or processed information (quantitative or qualitative) used for the analyses, case studies, + and/or descriptive interpretation in the preprint. Public data could include data posted to open-access + repositories, public archival library collection, or government archive. For data that is available under limited + circumstances (e.g., after signing a data sharing agreement), choose the ‘No’ option and use the comment box to + explain how others could access the data. +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + @let hasDataLinks = authorAssertionsForm.value.hasDataLinks!; + @if (hasDataLinks === ApplicabilityStatus.Unavailable || hasDataLinks === ApplicabilityStatus.NotApplicable) { + + @let whyNoDataControl = authorAssertionsForm.controls['whyNoData']; + @if (whyNoDataControl.errors?.['required'] && (whyNoDataControl.touched || whyNoDataControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } + } @else { +
+ +
+ } +
+
+ + +
+

Public Preregistration

+ +

+ A preregistration is a description of the research design and/or analysis plan that is created and registered + before researchers collected data or before they have seen/interacted with preexisting data. The description + should appear in a public registry (e.g., clinicaltrials.gov, OSF, AEA registry). +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ @let hasPreregLinks = authorAssertionsForm.value.hasPreregLinks!; + @if (hasPreregLinks === ApplicabilityStatus.Unavailable || hasPreregLinks === ApplicabilityStatus.NotApplicable) { + + @let hasPreregLinksControl = authorAssertionsForm.controls['hasPreregLinks']; + @if (hasPreregLinksControl.errors?.['required'] && (hasPreregLinksControl.touched || hasPreregLinksControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } + } @else { + + +
+ +
+ } +
+ +
+ + +
diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.scss b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.scss new file mode 100644 index 000000000..243cc50eb --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.scss @@ -0,0 +1,7 @@ +@use "assets/styles/variables" as var; + +.card { + @media (max-width: var.$breakpoint-sm) { + --p-card-body-padding: 0.75rem; + } +} diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts new file mode 100644 index 000000000..1d71b74f8 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthorAssertionsStepComponent } from './author-assertions-step.component'; + +describe('AuthorAssertionsComponent', () => { + let component: AuthorAssertionsStepComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AuthorAssertionsStepComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AuthorAssertionsStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts new file mode 100644 index 000000000..9af511445 --- /dev/null +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts @@ -0,0 +1,265 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Message } from 'primeng/message'; +import { RadioButton } from 'primeng/radiobutton'; +import { Select } from 'primeng/select'; +import { Textarea } from 'primeng/textarea'; +import { Tooltip } from 'primeng/tooltip'; + +import { NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, effect, HostListener, inject, output } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + AbstractControl, + FormArray, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidatorFn, + Validators, +} from '@angular/forms'; + +import { StringOrNull } from '@core/helpers'; +import { ArrayInputComponent } from '@osf/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component'; +import { formInputLimits } from '@osf/features/preprints/constants'; +import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums'; +import { SubmitPreprintSelectors, UpdatePreprint } from '@osf/features/preprints/store/submit-preprint'; +import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; +import { ToastService } from '@shared/services'; +import { CustomValidators } from '@shared/utils'; + +@Component({ + selector: 'osf-author-assertions-step', + imports: [ + Card, + FormsModule, + RadioButton, + ReactiveFormsModule, + Textarea, + Message, + TranslatePipe, + NgClass, + Button, + Tooltip, + Select, + ArrayInputComponent, + ], + templateUrl: './author-assertions-step.component.html', + styleUrl: './author-assertions-step.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AuthorAssertionsStepComponent { + private toastService = inject(ToastService); + private actions = createDispatchMap({ + updatePreprint: UpdatePreprint, + }); + + readonly CustomValidators = CustomValidators; + readonly ApplicabilityStatus = ApplicabilityStatus; + readonly inputLimits = formInputLimits; + readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; + readonly preregLinkOptions = Object.entries(PreregLinkInfo).map(([key, value]) => ({ + label: key, + value, + })); + readonly linkValidators = [CustomValidators.linkValidator(), CustomValidators.requiredTrimmed()]; + + createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + isUpdatingPreprint = select(SubmitPreprintSelectors.isPreprintSubmitting); + + readonly authorAssertionsForm = new FormGroup({ + hasCoi: new FormControl(this.createdPreprint()!.hasCoi || false, { + nonNullable: true, + validators: [], + }), + coiStatement: new FormControl(this.createdPreprint()!.coiStatement, { + nonNullable: false, + validators: [], + }), + hasDataLinks: new FormControl( + this.createdPreprint()!.hasDataLinks || ApplicabilityStatus.NotApplicable, + { + nonNullable: true, + validators: [], + } + ), + dataLinks: new FormArray( + this.createdPreprint()!.dataLinks?.map((link) => new FormControl(link)) || [] + ), + whyNoData: new FormControl(this.createdPreprint()!.whyNoData, { + nonNullable: false, + validators: [], + }), + hasPreregLinks: new FormControl( + this.createdPreprint()!.hasPreregLinks || ApplicabilityStatus.NotApplicable, + { + nonNullable: true, + validators: [], + } + ), + preregLinks: new FormArray( + this.createdPreprint()!.preregLinks?.map((link) => new FormControl(link)) || [] + ), + whyNoPrereg: new FormControl(this.createdPreprint()!.whyNoPrereg, { + nonNullable: false, + validators: [], + }), + preregLinkInfo: new FormControl(this.createdPreprint()!.preregLinkInfo, { + nonNullable: false, + validators: [], + }), + }); + + hasCoiValue = toSignal(this.authorAssertionsForm.controls['hasCoi'].valueChanges, { + initialValue: this.createdPreprint()!.hasCoi || false, + }); + hasDataLinks = toSignal(this.authorAssertionsForm.controls['hasDataLinks'].valueChanges, { + initialValue: this.createdPreprint()!.hasDataLinks || ApplicabilityStatus.NotApplicable, + }); + hasPreregLinks = toSignal(this.authorAssertionsForm.controls['hasPreregLinks'].valueChanges, { + initialValue: this.createdPreprint()!.hasPreregLinks || ApplicabilityStatus.NotApplicable, + }); + + nextClicked = output(); + + constructor() { + effect(() => { + const hasCoi = this.hasCoiValue(); + const coiStatementControl = this.authorAssertionsForm.controls['coiStatement']; + + if (hasCoi) { + this.enableAndSetValidators(coiStatementControl, [Validators.required]); + } else { + this.disableAndClearValidators(coiStatementControl); + } + + coiStatementControl.updateValueAndValidity(); + }); + + effect(() => { + const hasDataLinks = this.hasDataLinks(); + const whyNoDataControl = this.authorAssertionsForm.controls['whyNoData']; + const dataLinksControl = this.authorAssertionsForm.controls['dataLinks']; + + switch (hasDataLinks) { + case ApplicabilityStatus.Unavailable: + this.enableAndSetValidators(whyNoDataControl, [Validators.required]); + this.disableAndClearValidators(dataLinksControl); + break; + case ApplicabilityStatus.NotApplicable: + this.disableAndClearValidators(whyNoDataControl); + this.disableAndClearValidators(dataLinksControl); + break; + case ApplicabilityStatus.Applicable: + this.disableAndClearValidators(whyNoDataControl); + this.addAtLeastOneControl(dataLinksControl); + break; + } + whyNoDataControl.updateValueAndValidity(); + dataLinksControl.updateValueAndValidity(); + }); + + effect(() => { + const hasPreregLinks = this.hasPreregLinks(); + const whyNoPreregControl = this.authorAssertionsForm.controls['whyNoPrereg']; + const preregLinkInfoControl = this.authorAssertionsForm.controls['preregLinkInfo']; + const preregLinksControl = this.authorAssertionsForm.controls['preregLinks']; + + switch (hasPreregLinks) { + case ApplicabilityStatus.Unavailable: + this.enableAndSetValidators(whyNoPreregControl, [Validators.required]); + this.disableAndClearValidators(preregLinkInfoControl); + this.disableAndClearValidators(preregLinksControl); + break; + case ApplicabilityStatus.NotApplicable: + this.disableAndClearValidators(whyNoPreregControl); + this.disableAndClearValidators(preregLinkInfoControl); + this.disableAndClearValidators(preregLinksControl); + break; + case ApplicabilityStatus.Applicable: + this.disableAndClearValidators(whyNoPreregControl); + this.enableAndSetValidators(preregLinkInfoControl, [Validators.required]); + this.addAtLeastOneControl(preregLinksControl); + break; + } + whyNoPreregControl.updateValueAndValidity(); + preregLinkInfoControl.updateValueAndValidity(); + preregLinksControl.updateValueAndValidity(); + }); + } + + @HostListener('window:beforeunload', ['$event']) + public onBeforeUnload($event: BeforeUnloadEvent): boolean { + $event.preventDefault(); + return false; + } + + nextButtonClicked() { + const formValue = this.authorAssertionsForm.value; + + const hasCoi = formValue.hasCoi; + const coiStatement = formValue.coiStatement || null; + + const hasDataLinks = formValue.hasDataLinks; + const whyNoData = formValue.whyNoData || null; + const dataLinks: string[] = formValue.dataLinks || []; + + const hasPreregLinks = formValue.hasPreregLinks; + const whyNoPrereg = formValue.whyNoPrereg || null; + const preregLinks: string[] = formValue.preregLinks || []; + const preregLinkInfo = formValue.preregLinkInfo || undefined; + + this.actions + .updatePreprint(this.createdPreprint()!.id, { + hasCoi, + coiStatement, + hasDataLinks, + whyNoData, + dataLinks, + hasPreregLinks, + whyNoPrereg, + preregLinks, + preregLinkInfo, + }) + .subscribe({ + complete: () => { + this.toastService.showSuccess('Preprint saved successfully.'); + this.nextClicked.emit(); + }, + }); + } + + private disableAndClearValidators(control: AbstractControl) { + if (control instanceof FormArray) { + while (control.length !== 0) { + control.removeAt(0); + } + return; + } + + control.clearValidators(); + control.setValue(null); + control.disable(); + } + + private enableAndSetValidators(control: AbstractControl, validators: ValidatorFn[]) { + control.setValidators(validators); + control.enable(); + } + + private addAtLeastOneControl(formArray: FormArray) { + if (formArray.controls.length > 0) return; + + formArray.push( + new FormControl('', { + nonNullable: true, + validators: this.linkValidators, + }) + ); + } +} diff --git a/src/app/features/preprints/enums/applicability-status.enum.ts b/src/app/features/preprints/enums/applicability-status.enum.ts new file mode 100644 index 000000000..ec14aaa5a --- /dev/null +++ b/src/app/features/preprints/enums/applicability-status.enum.ts @@ -0,0 +1,5 @@ +export enum ApplicabilityStatus { + Applicable = 'available', + NotApplicable = 'not_applicable', + Unavailable = 'no', +} diff --git a/src/app/features/preprints/enums/index.ts b/src/app/features/preprints/enums/index.ts index 5b8a8b7f9..bf1bf51c4 100644 --- a/src/app/features/preprints/enums/index.ts +++ b/src/app/features/preprints/enums/index.ts @@ -1,2 +1,4 @@ +export { ApplicabilityStatus } from './applicability-status.enum'; export { PreprintFileSource } from './preprint-file-source.enum'; +export { PreregLinkInfo } from './prereg-link-info.enum'; export { SubmitSteps } from './submit-steps.enum'; diff --git a/src/app/features/preprints/enums/prereg-link-info.enum.ts b/src/app/features/preprints/enums/prereg-link-info.enum.ts new file mode 100644 index 000000000..b96928fbf --- /dev/null +++ b/src/app/features/preprints/enums/prereg-link-info.enum.ts @@ -0,0 +1,5 @@ +export enum PreregLinkInfo { + Analysis = 'prereg_analysis', + Designs = 'prereg_designs', + Both = 'prereg_both', +} diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 84d3770af..2661e1388 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -45,6 +45,15 @@ export class PreprintsMapper { copyrightHolders: response.attributes.license_record.copyright_holders.join(','), } : null, + hasCoi: response.attributes.has_coi, + coiStatement: response.attributes.conflict_of_interest_statement, + hasDataLinks: response.attributes.has_data_links, + dataLinks: response.attributes.data_links, + whyNoData: response.attributes.why_no_data, + hasPreregLinks: response.attributes.has_prereg_links, + whyNoPrereg: response.attributes.why_no_prereg, + preregLinks: response.attributes.prereg_links, + preregLinkInfo: response.attributes.prereg_link_info, }; } } diff --git a/src/app/features/preprints/models/preprint-json-api.models.ts b/src/app/features/preprints/models/preprint-json-api.models.ts index f826748ae..44ae1cd64 100644 --- a/src/app/features/preprints/models/preprint-json-api.models.ts +++ b/src/app/features/preprints/models/preprint-json-api.models.ts @@ -1,4 +1,5 @@ -import { StringOrNull } from '@core/helpers'; +import { BooleanOrNull, StringOrNull } from '@core/helpers'; +import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums'; import { LicenseRecordJsonApi } from '@shared/models'; export interface PreprintJsonApi { @@ -22,9 +23,15 @@ export interface PreprintJsonApi { date_last_transitioned: Date | null; version: number; is_latest_version: boolean; - has_coi: boolean; + has_coi: BooleanOrNull; conflict_of_interest_statement: StringOrNull; - has_data_links: boolean; + has_data_links: ApplicabilityStatus | null; + data_links: string[]; + why_no_data: StringOrNull; + has_prereg_links: ApplicabilityStatus | null; + why_no_prereg: StringOrNull; + prereg_links: string[]; + prereg_link_info: PreregLinkInfo | null; } export interface PreprintsRelationshipsJsonApi { diff --git a/src/app/features/preprints/models/preprint.models.ts b/src/app/features/preprints/models/preprint.models.ts index 4bc8a2be6..7f797cd77 100644 --- a/src/app/features/preprints/models/preprint.models.ts +++ b/src/app/features/preprints/models/preprint.models.ts @@ -1,4 +1,5 @@ -import { StringOrNull } from '@core/helpers'; +import { BooleanOrNull, StringOrNull } from '@core/helpers'; +import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums'; import { LicenseOptions } from '@shared/models'; export interface Preprint { @@ -18,6 +19,15 @@ export interface Preprint { primaryFileId: StringOrNull; licenseId: StringOrNull; licenseOptions: LicenseOptions | null; + hasCoi: BooleanOrNull; + coiStatement: StringOrNull; + hasDataLinks: ApplicabilityStatus | null; + dataLinks: string[]; + whyNoData: StringOrNull; + hasPreregLinks: ApplicabilityStatus | null; + whyNoPrereg: StringOrNull; + preregLinks: string[]; + preregLinkInfo: PreregLinkInfo | null; } export interface PreprintFilesLinks { diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html index da30ac767..0e34f65fd 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html @@ -42,6 +42,9 @@

{{ 'Add a ' + preprintProvider()!.preprintWor @case (SubmitStepsEnum.Metadata) { } + @case (SubmitStepsEnum.AuthorAssertions) { + + } @default {

No such step

} diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index a9d946efb..aea1de1c9 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -22,6 +22,7 @@ import { MetadataStepComponent, TitleAndAbstractStepComponent, } from '@osf/features/preprints/components'; +import { AuthorAssertionsStepComponent } from '@osf/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component'; import { submitPreprintSteps } from '@osf/features/preprints/constants'; import { SubmitSteps } from '@osf/features/preprints/enums'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; @@ -35,7 +36,14 @@ import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/utils'; @Component({ selector: 'osf-submit-preprint-stepper', - imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent, FileStepComponent, MetadataStepComponent], + imports: [ + Skeleton, + StepperComponent, + TitleAndAbstractStepComponent, + FileStepComponent, + MetadataStepComponent, + AuthorAssertionsStepComponent, + ], templateUrl: './submit-preprint-stepper.component.html', styleUrl: './submit-preprint-stepper.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index af95335f2..0668a8060 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -22,6 +22,15 @@ export class PreprintsService { doi: 'doi', customPublicationCitation: 'custom_publication_citation', tags: 'tags', + hasCoi: 'has_coi', + coiStatement: 'conflict_of_interest_statement', + hasDataLinks: 'has_data_links', + dataLinks: 'data_links', + whyNoData: 'why_no_data', + hasPreregLinks: 'has_prereg_links', + preregLinks: 'prereg_links', + whyNoPrereg: 'why_no_prereg', + preregLinkInfo: 'prereg_link_info', }; createPreprint(title: string, abstract: string, providerId: string) { diff --git a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.ts b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.ts index 5c21d6e6b..7475ef369 100644 --- a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.ts +++ b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.ts @@ -12,8 +12,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { linkValidator } from '@osf/core/helpers'; -import { IS_XSMALL } from '@osf/shared/utils'; +import { CustomValidators, IS_XSMALL } from '@osf/shared/utils'; import { DeveloperApp, DeveloperAppCreateUpdate, DeveloperAppForm, DeveloperAppFormFormControls } from '../../models'; import { CreateDeveloperApp, UpdateDeveloperApp } from '../../store'; @@ -41,14 +40,14 @@ export class DeveloperAppAddEditFormComponent implements OnInit { }), [DeveloperAppFormFormControls.ProjectHomePageUrl]: new FormControl('', { nonNullable: true, - validators: [Validators.required, linkValidator()], + validators: [Validators.required, CustomValidators.linkValidator()], }), [DeveloperAppFormFormControls.AppDescription]: new FormControl('', { nonNullable: false, }), [DeveloperAppFormFormControls.AuthorizationCallbackUrl]: new FormControl('', { nonNullable: true, - validators: [Validators.required, linkValidator()], + validators: [Validators.required, CustomValidators.linkValidator()], }), }); diff --git a/src/app/shared/components/text-input/text-input.component.ts b/src/app/shared/components/text-input/text-input.component.ts index 3b37fb94d..4a7a027d0 100644 --- a/src/app/shared/components/text-input/text-input.component.ts +++ b/src/app/shared/components/text-input/text-input.component.ts @@ -43,6 +43,10 @@ export class TextInputComponent { return { key: INPUT_VALIDATION_MESSAGES.email }; } + if (errors['link']) { + return { key: INPUT_VALIDATION_MESSAGES.link }; + } + if (errors['maxlength']) return { key: INPUT_VALIDATION_MESSAGES.maxLength, diff --git a/src/app/shared/constants/input-validation-messages.const.ts b/src/app/shared/constants/input-validation-messages.const.ts index ea17320af..8655c9dbc 100644 --- a/src/app/shared/constants/input-validation-messages.const.ts +++ b/src/app/shared/constants/input-validation-messages.const.ts @@ -4,4 +4,5 @@ export const INPUT_VALIDATION_MESSAGES = { maxLength: 'validation.maxLength', minLength: 'validation.minLength', invalidInput: 'validation.invalidInput', + link: 'validation.link', }; diff --git a/src/app/shared/utils/custom-form-validators.helper.ts b/src/app/shared/utils/custom-form-validators.helper.ts index 6e49ab71d..935966456 100644 --- a/src/app/shared/utils/custom-form-validators.helper.ts +++ b/src/app/shared/utils/custom-form-validators.helper.ts @@ -25,4 +25,19 @@ export class CustomValidators { return isValid ? null : { email: { value: control.value } }; }; } + + static linkValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (!value) { + return null; + } + + const urlPattern = /^(https):\/\/.+/i; + + const isValid = urlPattern.test(value); + + return isValid ? null : { link: true }; + }; + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c56fade29..ad612fb3b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1610,7 +1610,6 @@ "step2InfoText": "If your project includes components, you can select which components to include or exclude at the end of the registration." }, "selectProject": "Select your project", - "createDraft": "Create draft", "createdSuccessfully": "Draft created successfully" }, @@ -1745,7 +1744,8 @@ "email": "Please enter a valid email address.", "maxLength": "The field must be at most {{length}} characters.", "minLength": "The field must be at least {{length}} characters.", - "invalidInput": "Invalid input." + "invalidInput": "Invalid input.", + "link": "This field must start with https:// to be a valid url." }, "searchHelpTutorial": { "step1": {